1use crate::MonitorArgs;
2use crate::monitor::handle_monitor;
3use clap::Args;
4use clap::ValueEnum;
5use clap::{Parser, Subcommand};
6use color_eyre::Result;
7use color_eyre::eyre::eyre;
8use comfy_table::Attribute;
9use comfy_table::Cell;
10use comfy_table::Color;
11use comfy_table::ContentArrangement;
12use comfy_table::Table;
13use comfy_table::presets::UTF8_FULL;
14use console::Term;
15use console::style;
16use envx_core::ConflictStrategy;
17use envx_core::EnvWatcher;
18use envx_core::PathManager;
19use envx_core::ProjectConfig;
20use envx_core::ProjectManager;
21use envx_core::RequiredVar;
22use envx_core::SyncMode;
23use envx_core::ValidationReport;
24use envx_core::WatchConfig;
25use envx_core::env::split_wildcard_pattern;
26use envx_core::profile_manager::ProfileManager;
27use envx_core::snapshot_manager::SnapshotManager;
28use envx_core::{Analyzer, EnvVarManager, ExportFormat, Exporter, ImportFormat, Importer};
29use std::io::Write;
30use std::path::Path;
31use std::path::PathBuf;
32use std::time::Duration;
33#[derive(Parser)]
34#[command(name = "envx")]
35#[command(about = "System Environment Variable Manager")]
36#[command(version)]
37pub struct Cli {
38 #[command(subcommand)]
39 pub command: Commands,
40}
41
42#[derive(Subcommand)]
43pub enum Commands {
44 List {
46 #[arg(short, long)]
48 source: Option<String>,
49
50 #[arg(short = 'q', long)]
52 query: Option<String>,
53
54 #[arg(short, long, default_value = "table")]
56 format: String,
57
58 #[arg(long, default_value = "name")]
60 sort: String,
61
62 #[arg(long)]
64 names_only: bool,
65
66 #[arg(short, long)]
68 limit: Option<usize>,
69
70 #[arg(long)]
72 stats: bool,
73 },
74
75 Get {
77 pattern: String,
86
87 #[arg(short, long, default_value = "simple")]
89 format: String,
90 },
91
92 Set {
94 name: String,
96
97 value: String,
99
100 #[arg(short, long)]
102 temporary: bool,
103 },
104
105 Delete {
107 pattern: String,
109
110 #[arg(short, long)]
112 force: bool,
113 },
114
115 Analyze {
117 #[arg(short, long, default_value = "all")]
119 analysis_type: String,
120 },
121
122 #[command(visible_alias = "ui")]
124 Tui,
125
126 Path {
128 #[command(subcommand)]
129 action: Option<PathAction>,
130
131 #[arg(short, long)]
133 check: bool,
134
135 #[arg(short = 'v', long, default_value = "PATH")]
137 var: String,
138
139 #[arg(short = 'p', long)]
141 permanent: bool,
142 },
143
144 Export {
146 file: String,
148
149 #[arg(short = 'v', long)]
151 vars: Vec<String>,
152
153 #[arg(short, long)]
155 format: Option<String>,
156
157 #[arg(short, long)]
159 source: Option<String>,
160
161 #[arg(short, long)]
163 metadata: bool,
164
165 #[arg(long)]
167 force: bool,
168 },
169
170 Import {
172 file: String,
174
175 #[arg(short = 'v', long)]
177 vars: Vec<String>,
178
179 #[arg(short, long)]
181 format: Option<String>,
182
183 #[arg(short, long)]
185 permanent: bool,
186
187 #[arg(long)]
189 prefix: Option<String>,
190
191 #[arg(long)]
193 overwrite: bool,
194
195 #[arg(short = 'n', long)]
197 dry_run: bool,
198 },
199
200 Snapshot(SnapshotArgs),
202
203 Profile(ProfileArgs),
205
206 Project(ProjectArgs),
208
209 Rename(RenameArgs),
211
212 Replace(ReplaceArgs),
214
215 FindReplace(FindReplaceArgs),
217
218 Watch(WatchArgs),
220
221 Monitor(MonitorArgs),
223}
224
225#[derive(Subcommand)]
226pub enum PathAction {
227 Add {
229 directory: String,
231
232 #[arg(short, long)]
234 first: bool,
235
236 #[arg(short, long)]
238 create: bool,
239 },
240
241 Remove {
243 directory: String,
245
246 #[arg(short, long)]
248 all: bool,
249 },
250
251 Clean {
253 #[arg(short, long)]
255 dedupe: bool,
256
257 #[arg(short = 'n', long)]
259 dry_run: bool,
260 },
261
262 Dedupe {
264 #[arg(short, long)]
266 keep_first: bool,
267
268 #[arg(short = 'n', long)]
270 dry_run: bool,
271 },
272
273 Check {
275 #[arg(short, long)]
277 verbose: bool,
278 },
279
280 List {
282 #[arg(short, long)]
284 numbered: bool,
285
286 #[arg(short, long)]
288 check: bool,
289 },
290
291 Move {
293 from: String,
295
296 to: String,
298 },
299}
300
301#[derive(Args)]
302pub struct SnapshotArgs {
303 #[command(subcommand)]
304 pub command: SnapshotCommands,
305}
306
307#[derive(Subcommand)]
308pub enum SnapshotCommands {
309 Create {
311 name: String,
313 #[arg(short, long)]
315 description: Option<String>,
316 },
317 List,
319 Show {
321 snapshot: String,
323 },
324 Restore {
326 snapshot: String,
328 #[arg(short, long)]
330 force: bool,
331 },
332 Delete {
334 snapshot: String,
336 #[arg(short, long)]
338 force: bool,
339 },
340 Diff {
342 snapshot1: String,
344 snapshot2: String,
346 },
347}
348
349#[derive(Args)]
350pub struct ProfileArgs {
351 #[command(subcommand)]
352 pub command: ProfileCommands,
353}
354
355#[derive(Subcommand)]
356pub enum ProfileCommands {
357 Create {
359 name: String,
361 #[arg(short, long)]
363 description: Option<String>,
364 },
365 List,
367 Show {
369 name: Option<String>,
371 },
372 Switch {
374 name: String,
376 #[arg(short, long)]
378 apply: bool,
379 },
380 Add {
382 profile: String,
384 name: String,
386 value: String,
388 #[arg(short, long)]
390 override_system: bool,
391 },
392 Remove {
394 profile: String,
396 name: String,
398 },
399 Delete {
401 name: String,
403 #[arg(short, long)]
405 force: bool,
406 },
407 Export {
409 name: String,
411 #[arg(short, long)]
413 output: Option<String>,
414 },
415 Import {
417 file: String,
419 #[arg(short, long)]
421 name: Option<String>,
422 #[arg(short, long)]
424 overwrite: bool,
425 },
426 Apply {
428 name: String,
430 },
431}
432
433#[derive(Args)]
434pub struct ProjectArgs {
435 #[command(subcommand)]
436 pub command: ProjectCommands,
437}
438
439#[derive(Subcommand)]
440pub enum ProjectCommands {
441 Init {
443 #[arg(short, long)]
445 name: Option<String>,
446 },
447 Apply {
449 #[arg(short, long)]
451 force: bool,
452 },
453 Check,
455 Edit,
457 Info,
459 Run {
461 script: String,
463 },
464 Require {
466 name: String,
468 #[arg(short, long)]
470 description: Option<String>,
471 #[arg(short, long)]
473 pattern: Option<String>,
474 #[arg(short, long)]
476 example: Option<String>,
477 },
478}
479
480#[derive(Args)]
481pub struct RenameArgs {
482 pub pattern: String,
484
485 pub replacement: String,
487
488 #[arg(long)]
490 pub dry_run: bool,
491}
492
493#[derive(Args)]
494pub struct ReplaceArgs {
495 pub pattern: String,
497
498 pub value: String,
500
501 #[arg(long)]
503 pub dry_run: bool,
504}
505
506#[derive(Args)]
507pub struct FindReplaceArgs {
508 pub search: String,
510
511 pub replacement: String,
513
514 #[arg(short = 'p', long)]
516 pub pattern: Option<String>,
517
518 #[arg(long)]
520 pub dry_run: bool,
521}
522
523#[derive(Debug, Clone, ValueEnum)]
524pub enum Direction {
525 FileToSystem,
527 SystemToFile,
529 Bidirectional,
531}
532
533#[derive(Args, Clone)]
534pub struct WatchArgs {
535 #[arg(value_name = "PATH")]
537 pub paths: Vec<PathBuf>,
538
539 #[arg(short, long, value_enum, default_value = "file-to-system")]
541 pub direction: Direction,
542
543 #[arg(short, long)]
545 pub output: Option<PathBuf>,
546
547 #[arg(short, long)]
549 pub pattern: Vec<String>,
550
551 #[arg(long, default_value = "300")]
553 pub debounce: u64,
554
555 #[arg(short, long)]
557 pub log: Option<PathBuf>,
558
559 #[arg(short, long)]
561 pub vars: Vec<String>,
562
563 #[arg(short, long)]
565 pub quiet: bool,
566}
567
568pub fn execute(cli: Cli) -> Result<()> {
579 match cli.command {
580 Commands::List {
581 source,
582 query,
583 format,
584 sort,
585 names_only,
586 limit,
587 stats,
588 } => {
589 handle_list_command(
590 source.as_deref(),
591 query.as_deref(),
592 &format,
593 &sort,
594 names_only,
595 limit,
596 stats,
597 )?;
598 }
599
600 Commands::Get { pattern, format } => {
601 handle_get_command(&pattern, &format)?;
602 }
603
604 Commands::Set { name, value, temporary } => {
605 handle_set_command(&name, &value, temporary)?;
606 }
607
608 Commands::Delete { pattern, force } => {
609 handle_delete_command(&pattern, force)?;
610 }
611
612 Commands::Analyze { analysis_type } => {
613 handle_analyze_command(&analysis_type)?;
614 }
615
616 Commands::Tui => {
617 envx_tui::run()?;
619 }
620
621 Commands::Path {
622 action,
623 check,
624 var,
625 permanent,
626 } => {
627 handle_path_command(action, check, &var, permanent)?;
628 }
629
630 Commands::Export {
631 file,
632 vars,
633 format,
634 source,
635 metadata,
636 force,
637 } => {
638 handle_export(&file, &vars, format, source, metadata, force)?;
639 }
640
641 Commands::Import {
642 file,
643 vars,
644 format,
645 permanent,
646 prefix,
647 overwrite,
648 dry_run,
649 } => {
650 handle_import(&file, &vars, format, permanent, prefix.as_ref(), overwrite, dry_run)?;
651 }
652
653 Commands::Snapshot(args) => {
654 handle_snapshot(args)?;
655 }
656 Commands::Profile(args) => {
657 handle_profile(args)?;
658 }
659
660 Commands::Project(args) => {
661 handle_project(args)?;
662 }
663
664 Commands::Rename(args) => {
665 handle_rename(&args)?;
666 }
667
668 Commands::Replace(args) => {
669 handle_replace(&args)?;
670 }
671
672 Commands::FindReplace(args) => {
673 handle_find_replace(&args)?;
674 }
675
676 Commands::Watch(args) => {
677 handle_watch(&args)?;
678 }
679
680 Commands::Monitor(args) => {
681 handle_monitor(args)?;
682 }
683 }
684
685 Ok(())
686}
687
688fn handle_get_command(pattern: &str, format: &str) -> Result<()> {
689 let mut manager = EnvVarManager::new();
690 manager.load_all()?;
691
692 let vars = manager.get_pattern(pattern);
693
694 if vars.is_empty() {
695 eprintln!("No variables found matching pattern: {pattern}");
696 return Ok(());
697 }
698
699 match format {
700 "json" => {
701 println!("{}", serde_json::to_string_pretty(&vars)?);
702 }
703 "detailed" => {
704 for var in vars {
705 println!("Name: {}", var.name);
706 println!("Value: {}", var.value);
707 println!("Source: {:?}", var.source);
708 println!("Modified: {}", var.modified.format("%Y-%m-%d %H:%M:%S"));
709 if let Some(orig) = &var.original_value {
710 println!("Original: {orig}");
711 }
712 println!("---");
713 }
714 }
715 _ => {
716 for var in vars {
717 println!("{} = {}", var.name, var.value);
718 }
719 }
720 }
721 Ok(())
722}
723
724fn handle_set_command(name: &str, value: &str, temporary: bool) -> Result<()> {
725 let mut manager = EnvVarManager::new();
726 manager.load_all()?;
727
728 let permanent = !temporary;
729
730 manager.set(name, value, permanent)?;
731 if permanent {
732 println!("ā
Set {name} = \"{value}\"");
733 #[cfg(windows)]
734 println!("š Note: You may need to restart your terminal for changes to take effect");
735 } else {
736 println!("ā” Set {name} = \"{value}\" (temporary - current session only)");
737 }
738 Ok(())
739}
740
741fn handle_delete_command(pattern: &str, force: bool) -> Result<()> {
742 let mut manager = EnvVarManager::new();
743 manager.load_all()?;
744
745 let vars_to_delete: Vec<String> = manager
747 .get_pattern(pattern)
748 .into_iter()
749 .map(|v| v.name.clone())
750 .collect();
751
752 if vars_to_delete.is_empty() {
753 eprintln!("No variables found matching pattern: {pattern}");
754 return Ok(());
755 }
756
757 if !force && vars_to_delete.len() > 1 {
758 println!("About to delete {} variables:", vars_to_delete.len());
759 for name in &vars_to_delete {
760 println!(" - {name}");
761 }
762 print!("Continue? [y/N]: ");
763 std::io::stdout().flush()?;
764
765 let mut input = String::new();
766 std::io::stdin().read_line(&mut input)?;
767
768 if !input.trim().eq_ignore_ascii_case("y") {
769 println!("Cancelled.");
770 return Ok(());
771 }
772 }
773
774 for name in vars_to_delete {
776 manager.delete(&name)?;
777 println!("Deleted: {name}");
778 }
779 Ok(())
780}
781
782fn handle_analyze_command(analysis_type: &str) -> Result<()> {
783 let mut manager = EnvVarManager::new();
784 manager.load_all()?;
785 let vars = manager.list().into_iter().cloned().collect();
786 let analyzer = Analyzer::new(vars);
787
788 match analysis_type {
789 "duplicates" | "all" => {
790 let duplicates = analyzer.find_duplicates();
791 if !duplicates.is_empty() {
792 println!("Duplicate variables found:");
793 for (name, vars) in duplicates {
794 println!(" {}: {} instances", name, vars.len());
795 }
796 }
797 }
798 "invalid" => {
799 let validation = analyzer.validate_all();
800 for (name, result) in validation {
801 if !result.valid {
802 println!("Invalid variable: {name}");
803 for error in result.errors {
804 println!(" Error: {error}");
805 }
806 }
807 }
808 }
809 _ => {}
810 }
811 Ok(())
812}
813
814#[allow(clippy::too_many_lines)]
815fn handle_path_command(action: Option<PathAction>, check: bool, var: &str, permanent: bool) -> Result<()> {
816 let mut manager = EnvVarManager::new();
817 manager.load_all()?;
818
819 let path_var = manager.get(var).ok_or_else(|| eyre!("Variable '{}' not found", var))?;
821
822 let mut path_mgr = PathManager::new(&path_var.value);
823
824 if action.is_none() {
826 if check {
827 handle_path_check(&path_mgr, true);
828 }
829 handle_path_list(&path_mgr, false, false);
830 }
831
832 let command = action.expect("Action should be Some if we reach here");
833 match command {
834 PathAction::Add {
835 directory,
836 first,
837 create,
838 } => {
839 let path = Path::new(&directory);
840
841 if !path.exists() {
843 if create {
844 std::fs::create_dir_all(path)?;
845 println!("Created directory: {directory}");
846 } else if !path.exists() {
847 eprintln!("Warning: Directory does not exist: {directory}");
848 print!("Add anyway? [y/N]: ");
849 std::io::stdout().flush()?;
850
851 let mut input = String::new();
852 std::io::stdin().read_line(&mut input)?;
853
854 if !input.trim().eq_ignore_ascii_case("y") {
855 return Ok(());
856 }
857 }
858 }
859
860 if path_mgr.contains(&directory) {
862 println!("Directory already in {var}: {directory}");
863 return Ok(());
864 }
865
866 if first {
868 path_mgr.add_first(directory.clone());
869 println!("Added to beginning of {var}: {directory}");
870 } else {
871 path_mgr.add_last(directory.clone());
872 println!("Added to end of {var}: {directory}");
873 }
874
875 let new_value = path_mgr.to_string();
877 manager.set(var, &new_value, permanent)?;
878 }
879
880 PathAction::Remove { directory, all } => {
881 let removed = if all {
882 path_mgr.remove_all(&directory)
883 } else {
884 path_mgr.remove_first(&directory)
885 };
886
887 if removed > 0 {
888 println!("Removed {removed} occurrence(s) of: {directory}");
889 let new_value = path_mgr.to_string();
890 manager.set(var, &new_value, permanent)?;
891 } else {
892 println!("Directory not found in {var}: {directory}");
893 }
894 }
895
896 PathAction::Clean { dedupe, dry_run } => {
897 let invalid = path_mgr.get_invalid();
898 let duplicates = if dedupe { path_mgr.get_duplicates() } else { vec![] };
899
900 if invalid.is_empty() && duplicates.is_empty() {
901 println!("No invalid or duplicate entries found in {var}");
902 return Ok(());
903 }
904
905 if !invalid.is_empty() {
906 println!("Invalid/non-existent paths to remove:");
907 for path in &invalid {
908 println!(" - {path}");
909 }
910 }
911
912 if !duplicates.is_empty() {
913 println!("Duplicate paths to remove:");
914 for path in &duplicates {
915 println!(" - {path}");
916 }
917 }
918
919 if dry_run {
920 println!("\n(Dry run - no changes made)");
921 } else {
922 let removed_invalid = path_mgr.remove_invalid();
923 let removed_dupes = if dedupe {
924 path_mgr.deduplicate(false) } else {
926 0
927 };
928
929 println!("Removed {removed_invalid} invalid and {removed_dupes} duplicate entries");
930 let new_value = path_mgr.to_string();
931 manager.set(var, &new_value, permanent)?;
932 }
933 }
934
935 PathAction::Dedupe { keep_first, dry_run } => {
936 let duplicates = path_mgr.get_duplicates();
937
938 if duplicates.is_empty() {
939 println!("No duplicate entries found in {var}");
940 return Ok(());
941 }
942
943 println!("Duplicate paths to remove:");
944 for path in &duplicates {
945 println!(" - {path}");
946 }
947 println!(
948 "Strategy: keep {} occurrence",
949 if keep_first { "first" } else { "last" }
950 );
951
952 if dry_run {
953 println!("\n(Dry run - no changes made)");
954 } else {
955 let removed = path_mgr.deduplicate(keep_first);
956 println!("Removed {removed} duplicate entries");
957 let new_value = path_mgr.to_string();
958 manager.set(var, &new_value, permanent)?;
959 }
960 }
961
962 PathAction::Check { verbose } => {
963 handle_path_check(&path_mgr, verbose);
964 }
965
966 PathAction::List { numbered, check } => {
967 handle_path_list(&path_mgr, numbered, check);
968 }
969
970 PathAction::Move { from, to } => {
971 let from_idx = if let Ok(idx) = from.parse::<usize>() {
973 idx
974 } else {
975 path_mgr
976 .find_index(&from)
977 .ok_or_else(|| eyre!("Path not found: {}", from))?
978 };
979
980 let to_idx = match to.as_str() {
982 "first" => 0,
983 "last" => path_mgr.len() - 1,
984 _ => to.parse::<usize>().map_err(|_| eyre!("Invalid position: {}", to))?,
985 };
986
987 path_mgr.move_entry(from_idx, to_idx)?;
988 println!("Moved entry from position {from_idx} to {to_idx}");
989
990 let new_value = path_mgr.to_string();
991 manager.set(var, &new_value, permanent)?;
992 }
993 }
994
995 Ok(())
996}
997
998fn handle_path_check(path_mgr: &PathManager, verbose: bool) {
999 let entries = path_mgr.entries();
1000 let mut issues = Vec::new();
1001 let mut valid_count = 0;
1002
1003 for (idx, entry) in entries.iter().enumerate() {
1004 let path = Path::new(entry);
1005 let exists = path.exists();
1006 let is_dir = path.is_dir();
1007
1008 if verbose || !exists {
1009 let status = if !exists {
1010 issues.push(format!("Not found: {entry}"));
1011 "ā NOT FOUND"
1012 } else if !is_dir {
1013 issues.push(format!("Not a directory: {entry}"));
1014 "ā ļø NOT DIR"
1015 } else {
1016 valid_count += 1;
1017 "ā OK"
1018 };
1019
1020 if verbose {
1021 println!("[{idx:3}] {status} - {entry}");
1022 }
1023 } else if exists && is_dir {
1024 valid_count += 1;
1025 }
1026 }
1027
1028 println!("\nPATH Analysis:");
1030 println!(" Total entries: {}", entries.len());
1031 println!(" Valid entries: {valid_count}");
1032
1033 let duplicates = path_mgr.get_duplicates();
1034 if !duplicates.is_empty() {
1035 println!(" Duplicates: {} entries", duplicates.len());
1036 if verbose {
1037 for dup in &duplicates {
1038 println!(" - {dup}");
1039 }
1040 }
1041 }
1042
1043 let invalid = path_mgr.get_invalid();
1044 if !invalid.is_empty() {
1045 println!(" Invalid entries: {}", invalid.len());
1046 if verbose {
1047 for inv in &invalid {
1048 println!(" - {inv}");
1049 }
1050 }
1051 }
1052
1053 if issues.is_empty() {
1054 println!("\nā
No issues found!");
1055 } else {
1056 println!("\nā ļø {} issue(s) found", issues.len());
1057 if !verbose {
1058 println!("Run with --verbose for details");
1059 }
1060 }
1061}
1062
1063fn handle_path_list(path_mgr: &PathManager, numbered: bool, check: bool) {
1064 let entries = path_mgr.entries();
1065
1066 if entries.is_empty() {
1067 println!("PATH is empty");
1068 }
1069
1070 for (idx, entry) in entries.iter().enumerate() {
1071 let prefix = if numbered { format!("[{idx:3}] ") } else { String::new() };
1072
1073 let suffix = if check {
1074 let path = Path::new(entry);
1075 if !path.exists() {
1076 " [NOT FOUND]"
1077 } else if !path.is_dir() {
1078 " [NOT A DIRECTORY]"
1079 } else {
1080 ""
1081 }
1082 } else {
1083 ""
1084 };
1085
1086 println!("{prefix}{entry}{suffix}");
1087 }
1088}
1089
1090fn handle_export(
1091 file: &str,
1092 vars: &[String],
1093 format: Option<String>,
1094 source: Option<String>,
1095 metadata: bool,
1096 force: bool,
1097) -> Result<()> {
1098 if Path::new(&file).exists() && !force {
1100 print!("File '{file}' already exists. Overwrite? [y/N]: ");
1101 std::io::stdout().flush()?;
1102
1103 let mut input = String::new();
1104 std::io::stdin().read_line(&mut input)?;
1105
1106 if !input.trim().eq_ignore_ascii_case("y") {
1107 println!("Export cancelled.");
1108 return Ok(());
1109 }
1110 }
1111
1112 let mut manager = EnvVarManager::new();
1114 manager.load_all()?;
1115
1116 let mut vars_to_export = if vars.is_empty() {
1118 manager.list().into_iter().cloned().collect()
1119 } else {
1120 let mut selected = Vec::new();
1121 for pattern in vars {
1122 let matched = manager.get_pattern(pattern);
1123 selected.extend(matched.into_iter().cloned());
1124 }
1125 selected
1126 };
1127
1128 if let Some(src) = source {
1130 let source_filter = match src.as_str() {
1131 "system" => envx_core::EnvVarSource::System,
1132 "user" => envx_core::EnvVarSource::User,
1133 "process" => envx_core::EnvVarSource::Process,
1134 "shell" => envx_core::EnvVarSource::Shell,
1135 _ => return Err(eyre!("Invalid source: {}", src)),
1136 };
1137
1138 vars_to_export.retain(|v| v.source == source_filter);
1139 }
1140
1141 if vars_to_export.is_empty() {
1142 println!("No variables to export.");
1143 return Ok(());
1144 }
1145
1146 let export_format = if let Some(fmt) = format {
1148 match fmt.as_str() {
1149 "env" => ExportFormat::DotEnv,
1150 "json" => ExportFormat::Json,
1151 "yaml" | "yml" => ExportFormat::Yaml,
1152 "txt" | "text" => ExportFormat::Text,
1153 "ps1" | "powershell" => ExportFormat::PowerShell,
1154 "sh" | "bash" => ExportFormat::Shell,
1155 _ => return Err(eyre!("Unsupported format: {}", fmt)),
1156 }
1157 } else {
1158 ExportFormat::from_extension(file)?
1160 };
1161
1162 let exporter = Exporter::new(vars_to_export, metadata);
1164 exporter.export_to_file(file, export_format)?;
1165
1166 println!("Exported {} variables to '{}'", exporter.count(), file);
1167
1168 Ok(())
1169}
1170
1171fn handle_import(
1172 file: &str,
1173 vars: &[String],
1174 format: Option<String>,
1175 permanent: bool,
1176 prefix: Option<&String>,
1177 overwrite: bool,
1178 dry_run: bool,
1179) -> Result<()> {
1180 if !Path::new(&file).exists() {
1182 return Err(eyre!("File not found: {}", file));
1183 }
1184
1185 let import_format = if let Some(fmt) = format {
1187 match fmt.as_str() {
1188 "env" => ImportFormat::DotEnv,
1189 "json" => ImportFormat::Json,
1190 "yaml" | "yml" => ImportFormat::Yaml,
1191 "txt" | "text" => ImportFormat::Text,
1192 _ => return Err(eyre!("Unsupported format: {}", fmt)),
1193 }
1194 } else {
1195 ImportFormat::from_extension(file)?
1197 };
1198
1199 let mut importer = Importer::new();
1201 importer.import_from_file(file, import_format)?;
1202
1203 if !vars.is_empty() {
1205 importer.filter_by_patterns(vars);
1206 }
1207
1208 if let Some(pfx) = &prefix {
1210 importer.add_prefix(pfx);
1211 }
1212
1213 let import_vars = importer.get_variables();
1215
1216 if import_vars.is_empty() {
1217 println!("No variables to import.");
1218 return Ok(());
1219 }
1220
1221 let mut manager = EnvVarManager::new();
1223 manager.load_all()?;
1224
1225 let mut conflicts = Vec::new();
1226 for (name, _) in &import_vars {
1227 if manager.get(name).is_some() {
1228 conflicts.push(name.clone());
1229 }
1230 }
1231
1232 if !conflicts.is_empty() && !overwrite && !dry_run {
1233 println!("The following variables already exist:");
1234 for name in &conflicts {
1235 println!(" - {name}");
1236 }
1237
1238 print!("Overwrite existing variables? [y/N]: ");
1239 std::io::stdout().flush()?;
1240
1241 let mut input = String::new();
1242 std::io::stdin().read_line(&mut input)?;
1243
1244 if !input.trim().eq_ignore_ascii_case("y") {
1245 println!("Import cancelled.");
1246 return Ok(());
1247 }
1248 }
1249
1250 if dry_run {
1252 println!("Would import {} variables:", import_vars.len());
1253 for (name, value) in &import_vars {
1254 let status = if conflicts.contains(name) {
1255 " [OVERWRITE]"
1256 } else {
1257 " [NEW]"
1258 };
1259 println!(
1260 " {} = {}{}",
1261 name,
1262 if value.len() > 50 {
1263 format!("{}...", &value[..50])
1264 } else {
1265 value.clone()
1266 },
1267 status
1268 );
1269 }
1270 println!("\n(Dry run - no changes made)");
1271 } else {
1272 let mut imported = 0;
1274 let mut failed = 0;
1275
1276 for (name, value) in import_vars {
1277 match manager.set(&name, &value, permanent) {
1278 Ok(()) => imported += 1,
1279 Err(e) => {
1280 eprintln!("Failed to import {name}: {e}");
1281 failed += 1;
1282 }
1283 }
1284 }
1285
1286 println!("Imported {imported} variables");
1287 if failed > 0 {
1288 println!("Failed to import {failed} variables");
1289 }
1290 }
1291
1292 Ok(())
1293}
1294
1295fn handle_list_command(
1296 source: Option<&str>,
1297 query: Option<&str>,
1298 format: &str,
1299 sort: &str,
1300 names_only: bool,
1301 limit: Option<usize>,
1302 stats: bool,
1303) -> Result<()> {
1304 let mut manager = EnvVarManager::new();
1305 manager.load_all()?;
1306
1307 let mut vars = if let Some(q) = &query {
1309 manager.search(q)
1310 } else if let Some(src) = source {
1311 let source_filter = match src {
1312 "system" => envx_core::EnvVarSource::System,
1313 "user" => envx_core::EnvVarSource::User,
1314 "process" => envx_core::EnvVarSource::Process,
1315 "shell" => envx_core::EnvVarSource::Shell,
1316 _ => return Err(eyre!("Invalid source: {}", src)),
1317 };
1318 manager.filter_by_source(&source_filter)
1319 } else {
1320 manager.list()
1321 };
1322
1323 match sort {
1325 "name" => vars.sort_by(|a, b| a.name.cmp(&b.name)),
1326 "value" => vars.sort_by(|a, b| a.value.cmp(&b.value)),
1327 "source" => vars.sort_by(|a, b| format!("{:?}", a.source).cmp(&format!("{:?}", b.source))),
1328 _ => {}
1329 }
1330
1331 let total_count = vars.len();
1333 if let Some(lim) = limit {
1334 vars.truncate(lim);
1335 }
1336
1337 if stats || (format == "table" && !names_only) {
1339 print_statistics(&manager, &vars, total_count, query, source);
1340 }
1341
1342 if names_only {
1344 for var in vars {
1345 println!("{}", var.name);
1346 }
1347 return Ok(());
1348 }
1349
1350 match format {
1352 "json" => {
1353 println!("{}", serde_json::to_string_pretty(&vars)?);
1354 }
1355 "simple" => {
1356 for var in vars {
1357 println!("{} = {}", style(&var.name).cyan(), var.value);
1358 }
1359 }
1360 "compact" => {
1361 for var in vars {
1362 let source_str = format_source_compact(&var.source);
1363 println!(
1364 "{} {} = {}",
1365 source_str,
1366 style(&var.name).bright(),
1367 style(truncate_value(&var.value, 60)).dim()
1368 );
1369 }
1370 }
1371 _ => {
1372 print_table(vars, limit.is_some());
1373 }
1374 }
1375
1376 if let Some(lim) = limit {
1378 if total_count > lim {
1379 println!(
1380 "\n{}",
1381 style(format!(
1382 "Showing {lim} of {total_count} total variables. Use --limit to see more."
1383 ))
1384 .yellow()
1385 );
1386 }
1387 }
1388
1389 Ok(())
1390}
1391
1392pub fn handle_snapshot(args: SnapshotArgs) -> Result<()> {
1404 let snapshot_manager = SnapshotManager::new()?;
1405 let mut env_manager = EnvVarManager::new();
1406 env_manager.load_all()?;
1407
1408 match args.command {
1409 SnapshotCommands::Create { name, description } => {
1410 let vars = env_manager.list().into_iter().cloned().collect();
1411 let snapshot = snapshot_manager.create(name, description, vars)?;
1412 println!("ā
Created snapshot: {} (ID: {})", snapshot.name, snapshot.id);
1413 }
1414 SnapshotCommands::List => {
1415 let snapshots = snapshot_manager.list()?;
1416 if snapshots.is_empty() {
1417 println!("No snapshots found.");
1418 return Ok(());
1419 }
1420
1421 let mut table = Table::new();
1422 table.set_header(vec!["Name", "ID", "Created", "Variables", "Description"]);
1423
1424 for snapshot in snapshots {
1425 table.add_row(vec![
1426 snapshot.name,
1427 snapshot.id[..8].to_string(),
1428 snapshot.created_at.format("%Y-%m-%d %H:%M").to_string(),
1429 snapshot.variables.len().to_string(),
1430 snapshot.description.unwrap_or_default(),
1431 ]);
1432 }
1433
1434 println!("{table}");
1435 }
1436 SnapshotCommands::Show { snapshot } => {
1437 let snap = snapshot_manager.get(&snapshot)?;
1438 println!("Snapshot: {}", snap.name);
1439 println!("ID: {}", snap.id);
1440 println!("Created: {}", snap.created_at.format("%Y-%m-%d %H:%M:%S"));
1441 println!("Description: {}", snap.description.unwrap_or_default());
1442 println!("Variables: {}", snap.variables.len());
1443
1444 println!("\nFirst 10 variables:");
1446 for (i, (name, var)) in snap.variables.iter().take(10).enumerate() {
1447 println!(" {}. {} = {}", i + 1, name, var.value);
1448 }
1449
1450 if snap.variables.len() > 10 {
1451 println!(" ... and {} more", snap.variables.len() - 10);
1452 }
1453 }
1454 SnapshotCommands::Restore { snapshot, force } => {
1455 if !force {
1456 print!("ā ļø This will replace all current environment variables. Continue? [y/N] ");
1457 std::io::Write::flush(&mut std::io::stdout())?;
1458
1459 let mut input = String::new();
1460 std::io::stdin().read_line(&mut input)?;
1461 if !input.trim().eq_ignore_ascii_case("y") {
1462 println!("Cancelled.");
1463 return Ok(());
1464 }
1465 }
1466
1467 snapshot_manager.restore(&snapshot, &mut env_manager)?;
1468 println!("ā
Restored from snapshot: {snapshot}");
1469 }
1470 SnapshotCommands::Delete { snapshot, force } => {
1471 if !force {
1472 print!("ā ļø Delete snapshot '{snapshot}'? [y/N] ");
1473 std::io::Write::flush(&mut std::io::stdout())?;
1474
1475 let mut input = String::new();
1476 std::io::stdin().read_line(&mut input)?;
1477 if !input.trim().eq_ignore_ascii_case("y") {
1478 println!("Cancelled.");
1479 return Ok(());
1480 }
1481 }
1482
1483 snapshot_manager.delete(&snapshot)?;
1484 println!("ā
Deleted snapshot: {snapshot}");
1485 }
1486 SnapshotCommands::Diff { snapshot1, snapshot2 } => {
1487 let diff = snapshot_manager.diff(&snapshot1, &snapshot2)?;
1488
1489 if diff.added.is_empty() && diff.removed.is_empty() && diff.modified.is_empty() {
1490 println!("No differences found between snapshots.");
1491 return Ok(());
1492 }
1493
1494 if !diff.added.is_empty() {
1495 println!("ā Added in {snapshot2}:");
1496 for (name, var) in &diff.added {
1497 println!(" {} = {}", name, var.value);
1498 }
1499 }
1500
1501 if !diff.removed.is_empty() {
1502 println!("\nā Removed in {snapshot2}:");
1503 for (name, var) in &diff.removed {
1504 println!(" {} = {}", name, var.value);
1505 }
1506 }
1507
1508 if !diff.modified.is_empty() {
1509 println!("\nš Modified:");
1510 for (name, (old, new)) in &diff.modified {
1511 println!(" {name}:");
1512 println!(" Old: {}", old.value);
1513 println!(" New: {}", new.value);
1514 }
1515 }
1516 }
1517 }
1518
1519 Ok(())
1520}
1521
1522pub fn handle_profile(args: ProfileArgs) -> Result<()> {
1535 let mut profile_manager = ProfileManager::new()?;
1536 let mut env_manager = EnvVarManager::new();
1537 env_manager.load_all()?;
1538
1539 match args.command {
1540 ProfileCommands::Create { name, description } => {
1541 handle_profile_create(&mut profile_manager, &name, description)?;
1542 }
1543 ProfileCommands::List => {
1544 handle_profile_list(&profile_manager);
1545 }
1546 ProfileCommands::Show { name } => {
1547 handle_profile_show(&profile_manager, name)?;
1548 }
1549 ProfileCommands::Switch { name, apply } => {
1550 handle_profile_switch(&mut profile_manager, &mut env_manager, &name, apply)?;
1551 }
1552 ProfileCommands::Add {
1553 profile,
1554 name,
1555 value,
1556 override_system,
1557 } => {
1558 handle_profile_add(&mut profile_manager, &profile, &name, &value, override_system)?;
1559 }
1560 ProfileCommands::Remove { profile, name } => {
1561 handle_profile_remove(&mut profile_manager, &profile, &name)?;
1562 }
1563 ProfileCommands::Delete { name, force } => {
1564 handle_profile_delete(&mut profile_manager, &name, force)?;
1565 }
1566 ProfileCommands::Export { name, output } => {
1567 handle_profile_export(&profile_manager, &name, output)?;
1568 }
1569 ProfileCommands::Import { file, name, overwrite } => {
1570 handle_profile_import(&mut profile_manager, &file, name, overwrite)?;
1571 }
1572 ProfileCommands::Apply { name } => {
1573 handle_profile_apply(&mut profile_manager, &mut env_manager, &name)?;
1574 }
1575 }
1576
1577 Ok(())
1578}
1579
1580fn handle_profile_create(profile_manager: &mut ProfileManager, name: &str, description: Option<String>) -> Result<()> {
1581 profile_manager.create(name.to_string(), description)?;
1582 println!("ā
Created profile: {name}");
1583 Ok(())
1584}
1585
1586fn handle_profile_list(profile_manager: &ProfileManager) {
1587 let profiles = profile_manager.list();
1588 if profiles.is_empty() {
1589 println!("No profiles found.");
1590 }
1591
1592 let active = profile_manager.active().map(|p| &p.name);
1593 let mut table = Table::new();
1594 table.set_header(vec!["Name", "Variables", "Created", "Description", "Status"]);
1595
1596 for profile in profiles {
1597 let status = if active == Some(&profile.name) {
1598 "ā Active"
1599 } else {
1600 ""
1601 };
1602
1603 table.add_row(vec![
1604 profile.name.clone(),
1605 profile.variables.len().to_string(),
1606 profile.created_at.format("%Y-%m-%d").to_string(),
1607 profile.description.clone().unwrap_or_default(),
1608 status.to_string(),
1609 ]);
1610 }
1611
1612 println!("{table}");
1613}
1614
1615fn handle_profile_show(profile_manager: &ProfileManager, name: Option<String>) -> Result<()> {
1616 let profile = if let Some(name) = name {
1617 profile_manager
1618 .get(&name)
1619 .ok_or_else(|| color_eyre::eyre::eyre!("Profile '{}' not found", name))?
1620 } else {
1621 profile_manager
1622 .active()
1623 .ok_or_else(|| color_eyre::eyre::eyre!("No active profile"))?
1624 };
1625
1626 println!("Profile: {}", profile.name);
1627 println!("Description: {}", profile.description.as_deref().unwrap_or(""));
1628 println!("Created: {}", profile.created_at.format("%Y-%m-%d %H:%M:%S"));
1629 println!("Updated: {}", profile.updated_at.format("%Y-%m-%d %H:%M:%S"));
1630 if let Some(parent) = &profile.parent {
1631 println!("Inherits from: {parent}");
1632 }
1633 println!("\nVariables:");
1634
1635 for (name, var) in &profile.variables {
1636 let status = if var.enabled { "ā" } else { "ā" };
1637 let override_flag = if var.override_system { " [override]" } else { "" };
1638 println!(" {} {} = {}{}", status, name, var.value, override_flag);
1639 }
1640 Ok(())
1641}
1642
1643fn handle_profile_switch(
1644 profile_manager: &mut ProfileManager,
1645 env_manager: &mut EnvVarManager,
1646 name: &str,
1647 apply: bool,
1648) -> Result<()> {
1649 profile_manager.switch(name)?;
1650 println!("ā
Switched to profile: {name}");
1651
1652 if apply {
1653 profile_manager.apply(name, env_manager)?;
1654 println!("ā
Applied profile variables");
1655 }
1656 Ok(())
1657}
1658
1659fn handle_profile_add(
1660 profile_manager: &mut ProfileManager,
1661 profile: &str,
1662 name: &str,
1663 value: &str,
1664 override_system: bool,
1665) -> Result<()> {
1666 let prof = profile_manager
1667 .get_mut(profile)
1668 .ok_or_else(|| color_eyre::eyre::eyre!("Profile '{}' not found", profile))?;
1669
1670 prof.add_var(name.to_string(), value.to_string(), override_system);
1671 profile_manager.save()?;
1672
1673 println!("ā
Added {name} to profile {profile}");
1674 Ok(())
1675}
1676
1677fn handle_profile_remove(profile_manager: &mut ProfileManager, profile: &str, name: &str) -> Result<()> {
1678 let prof = profile_manager
1679 .get_mut(profile)
1680 .ok_or_else(|| color_eyre::eyre::eyre!("Profile '{}' not found", profile))?;
1681
1682 prof.remove_var(name)
1683 .ok_or_else(|| color_eyre::eyre::eyre!("Variable '{}' not found in profile", name))?;
1684
1685 profile_manager.save()?;
1686 println!("ā
Removed {name} from profile {profile}");
1687 Ok(())
1688}
1689
1690fn handle_profile_delete(profile_manager: &mut ProfileManager, name: &str, force: bool) -> Result<()> {
1691 if !force {
1692 print!("ā ļø Delete profile '{name}'? [y/N] ");
1693 std::io::Write::flush(&mut std::io::stdout())?;
1694
1695 let mut input = String::new();
1696 std::io::stdin().read_line(&mut input)?;
1697 if !input.trim().eq_ignore_ascii_case("y") {
1698 println!("Cancelled.");
1699 return Ok(());
1700 }
1701 }
1702
1703 profile_manager.delete(name)?;
1704 println!("ā
Deleted profile: {name}");
1705 Ok(())
1706}
1707
1708fn handle_profile_export(profile_manager: &ProfileManager, name: &str, output: Option<String>) -> Result<()> {
1709 let json = profile_manager.export(name)?;
1710
1711 if let Some(path) = output {
1712 std::fs::write(path, json)?;
1713 println!("ā
Exported profile to file");
1714 } else {
1715 println!("{json}");
1716 }
1717 Ok(())
1718}
1719
1720fn handle_profile_import(
1721 profile_manager: &mut ProfileManager,
1722 file: &str,
1723 name: Option<String>,
1724 overwrite: bool,
1725) -> Result<()> {
1726 let content = std::fs::read_to_string(file)?;
1727 let import_name = name.unwrap_or_else(|| "imported".to_string());
1728
1729 profile_manager.import(import_name.clone(), &content, overwrite)?;
1730 println!("ā
Imported profile: {import_name}");
1731 Ok(())
1732}
1733
1734fn handle_profile_apply(
1735 profile_manager: &mut ProfileManager,
1736 env_manager: &mut EnvVarManager,
1737 name: &str,
1738) -> Result<()> {
1739 profile_manager.apply(name, env_manager)?;
1740 println!("ā
Applied profile: {name}");
1741 Ok(())
1742}
1743
1744fn print_statistics(
1745 manager: &EnvVarManager,
1746 filtered_vars: &[&envx_core::EnvVar],
1747 total_count: usize,
1748 query: Option<&str>,
1749 source: Option<&str>,
1750) {
1751 let _term = Term::stdout();
1752
1753 let system_count = manager.filter_by_source(&envx_core::EnvVarSource::System).len();
1755 let user_count = manager.filter_by_source(&envx_core::EnvVarSource::User).len();
1756 let process_count = manager.filter_by_source(&envx_core::EnvVarSource::Process).len();
1757 let shell_count = manager.filter_by_source(&envx_core::EnvVarSource::Shell).len();
1758
1759 println!("{}", style("ā".repeat(60)).blue().bold());
1761 println!("{}", style("Environment Variables Summary").cyan().bold());
1762 println!("{}", style("ā".repeat(60)).blue().bold());
1763
1764 if query.is_some() || source.is_some() {
1766 print!(" {} ", style("Filter:").yellow());
1767 if let Some(q) = query {
1768 print!("query='{}' ", style(q).green());
1769 }
1770 if let Some(s) = source {
1771 print!("source={} ", style(s).green());
1772 }
1773 println!();
1774 println!(
1775 " {} {}/{} variables",
1776 style("Showing:").yellow(),
1777 style(filtered_vars.len()).green().bold(),
1778 total_count
1779 );
1780 } else {
1781 println!(
1782 " {} {} variables",
1783 style("Total:").yellow(),
1784 style(total_count).green().bold()
1785 );
1786 }
1787
1788 println!();
1789 println!(" {} By Source:", style("āŗ").cyan());
1790
1791 let max_count = system_count.max(user_count).max(process_count).max(shell_count);
1793 let bar_width = 30;
1794
1795 print_source_bar("System", system_count, max_count, bar_width, "red");
1796 print_source_bar("User", user_count, max_count, bar_width, "yellow");
1797 print_source_bar("Process", process_count, max_count, bar_width, "green");
1798 print_source_bar("Shell", shell_count, max_count, bar_width, "cyan");
1799
1800 println!("{}", style("ā".repeat(60)).blue());
1801 println!();
1802}
1803
1804fn print_source_bar(label: &str, count: usize, max: usize, width: usize, color: &str) {
1805 let filled = if max > 0 { (count * width / max).max(1) } else { 0 };
1806
1807 let bar = "ā".repeat(filled);
1808 let empty = "ā".repeat(width - filled);
1809
1810 let colored_bar = match color {
1811 "red" => style(bar).red(),
1812 "yellow" => style(bar).yellow(),
1813 "green" => style(bar).green(),
1814 "cyan" => style(bar).cyan(),
1815 _ => style(bar).white(),
1816 };
1817
1818 println!(
1819 " {:10} {} {}{} {}",
1820 style(label).bold(),
1821 colored_bar,
1822 style(empty).dim(),
1823 style(format!(" {count:4}")).bold(),
1824 style("vars").dim()
1825 );
1826}
1827
1828fn print_table(vars: Vec<&envx_core::EnvVar>, _is_limited: bool) {
1829 if vars.is_empty() {
1830 println!("{}", style("No environment variables found.").yellow());
1831 }
1832
1833 let mut table = Table::new();
1834
1835 table
1837 .set_content_arrangement(ContentArrangement::Dynamic)
1838 .set_width(120)
1839 .set_header(vec![
1840 Cell::new("Source").add_attribute(Attribute::Bold).fg(Color::Cyan),
1841 Cell::new("Name").add_attribute(Attribute::Bold).fg(Color::Cyan),
1842 Cell::new("Value").add_attribute(Attribute::Bold).fg(Color::Cyan),
1843 ]);
1844
1845 for var in vars {
1847 let (source_str, source_color) = format_source(&var.source);
1848 let truncated_value = truncate_value(&var.value, 50);
1849
1850 table.add_row(vec![
1851 Cell::new(source_str).fg(source_color),
1852 Cell::new(&var.name).fg(Color::White),
1853 Cell::new(truncated_value).fg(Color::Grey),
1854 ]);
1855 }
1856
1857 println!("{table}");
1858}
1859
1860fn format_source(source: &envx_core::EnvVarSource) -> (String, Color) {
1861 match source {
1862 envx_core::EnvVarSource::System => ("System".to_string(), Color::Red),
1863 envx_core::EnvVarSource::User => ("User".to_string(), Color::Yellow),
1864 envx_core::EnvVarSource::Process => ("Process".to_string(), Color::Green),
1865 envx_core::EnvVarSource::Shell => ("Shell".to_string(), Color::Cyan),
1866 envx_core::EnvVarSource::Application(app) => (format!("App:{app}"), Color::Magenta),
1867 }
1868}
1869
1870fn format_source_compact(source: &envx_core::EnvVarSource) -> console::StyledObject<String> {
1871 match source {
1872 envx_core::EnvVarSource::System => style("[SYS]".to_string()).red().bold(),
1873 envx_core::EnvVarSource::User => style("[USR]".to_string()).yellow().bold(),
1874 envx_core::EnvVarSource::Process => style("[PRC]".to_string()).green().bold(),
1875 envx_core::EnvVarSource::Shell => style("[SHL]".to_string()).cyan().bold(),
1876 envx_core::EnvVarSource::Application(app) => style(format!("[{}]", &app[..3.min(app.len())].to_uppercase()))
1877 .magenta()
1878 .bold(),
1879 }
1880}
1881
1882fn truncate_value(value: &str, max_len: usize) -> String {
1883 if value.len() <= max_len {
1884 value.to_string()
1885 } else {
1886 format!("{}...", &value[..max_len - 3])
1887 }
1888}
1889
1890#[allow(clippy::too_many_lines)]
1904pub fn handle_project(args: ProjectArgs) -> Result<()> {
1905 match args.command {
1906 ProjectCommands::Init { name } => {
1907 let manager = ProjectManager::new()?;
1908 manager.init(name)?;
1909 }
1910
1911 ProjectCommands::Apply { force } => {
1912 let mut project = ProjectManager::new()?;
1913 let mut env_manager = EnvVarManager::new();
1914 let mut profile_manager = ProfileManager::new()?;
1915
1916 if let Some(project_dir) = project.find_and_load()? {
1917 println!("š Found project at: {}", project_dir.display());
1918
1919 let report = project.validate(&env_manager)?;
1921
1922 if !report.success && !force {
1923 print_validation_report(&report);
1924 return Err(color_eyre::eyre::eyre!(
1925 "Validation failed. Use --force to apply anyway."
1926 ));
1927 }
1928
1929 project.apply(&mut env_manager, &mut profile_manager)?;
1931 println!("ā
Applied project configuration");
1932
1933 if !report.warnings.is_empty() {
1934 println!("\nā ļø Warnings:");
1935 for warning in &report.warnings {
1936 println!(" - {}: {}", warning.var_name, warning.message);
1937 }
1938 }
1939 } else {
1940 return Err(color_eyre::eyre::eyre!(
1941 "No .envx/config.yaml found in current or parent directories"
1942 ));
1943 }
1944 }
1945
1946 ProjectCommands::Check => {
1947 let mut project = ProjectManager::new()?;
1948 let env_manager = EnvVarManager::new();
1949
1950 if project.find_and_load()?.is_some() {
1951 let report = project.validate(&env_manager)?;
1952 print_validation_report(&report);
1953
1954 if !report.success {
1955 std::process::exit(1);
1956 }
1957 } else {
1958 return Err(color_eyre::eyre::eyre!("No project configuration found"));
1959 }
1960 }
1961
1962 ProjectCommands::Edit => {
1963 let _ = ProjectManager::new()?;
1964 let config_path = std::env::current_dir()?.join(".envx").join("config.yaml");
1965
1966 if !config_path.exists() {
1967 return Err(color_eyre::eyre::eyre!(
1968 "No .envx/config.yaml found. Run 'envx init' first."
1969 ));
1970 }
1971
1972 #[cfg(windows)]
1973 {
1974 std::process::Command::new("notepad").arg(&config_path).spawn()?;
1975 }
1976
1977 #[cfg(unix)]
1978 {
1979 let editor = std::env::var("EDITOR").unwrap_or_else(|_| "nano".to_string());
1980 std::process::Command::new(editor).arg(&config_path).spawn()?;
1981 }
1982
1983 println!("š Opening config in editor...");
1984 }
1985
1986 ProjectCommands::Info => {
1987 let mut project = ProjectManager::new()?;
1988
1989 if let Some(project_dir) = project.find_and_load()? {
1990 let config_path = project_dir.join(".envx").join("config.yaml");
1992 let content = std::fs::read_to_string(&config_path)?;
1993
1994 println!("š Project Directory: {}", project_dir.display());
1995 println!("\nš Configuration:");
1996 println!("{content}");
1997 } else {
1998 return Err(color_eyre::eyre::eyre!("No project configuration found"));
1999 }
2000 }
2001
2002 ProjectCommands::Run { script } => {
2003 let mut project = ProjectManager::new()?;
2004 let mut env_manager = EnvVarManager::new();
2005
2006 if project.find_and_load()?.is_some() {
2007 project.run_script(&script, &mut env_manager)?;
2008 println!("ā
Script '{script}' completed");
2009 } else {
2010 return Err(color_eyre::eyre::eyre!("No project configuration found"));
2011 }
2012 }
2013
2014 ProjectCommands::Require {
2015 name,
2016 description,
2017 pattern,
2018 example,
2019 } => {
2020 let config_path = std::env::current_dir()?.join(".envx").join("config.yaml");
2021
2022 if !config_path.exists() {
2023 return Err(color_eyre::eyre::eyre!(
2024 "No .envx/config.yaml found. Run 'envx init' first."
2025 ));
2026 }
2027
2028 let mut config = ProjectConfig::load(&config_path)?;
2030 config.required.push(RequiredVar {
2031 name: name.clone(),
2032 description,
2033 pattern,
2034 example,
2035 });
2036 config.save(&config_path)?;
2037
2038 println!("ā
Added required variable: {name}");
2039 }
2040 }
2041
2042 Ok(())
2043}
2044
2045fn print_validation_report(report: &ValidationReport) {
2046 if report.success {
2047 println!("ā
All required variables are set!");
2048 return;
2049 }
2050
2051 if !report.missing.is_empty() {
2052 println!("ā Missing required variables:");
2053 let mut table = Table::new();
2054 table.set_header(vec!["Variable", "Description", "Example"]);
2055
2056 for var in &report.missing {
2057 table.add_row(vec![
2058 var.name.clone(),
2059 var.description.clone().unwrap_or_default(),
2060 var.example.clone().unwrap_or_default(),
2061 ]);
2062 }
2063
2064 println!("{table}");
2065 }
2066
2067 if !report.errors.is_empty() {
2068 println!("\nā Validation errors:");
2069 for error in &report.errors {
2070 println!(" - {}: {}", error.var_name, error.message);
2071 }
2072 }
2073}
2074
2075pub fn handle_rename(args: &RenameArgs) -> Result<()> {
2086 let mut manager = EnvVarManager::new();
2087 manager.load_all()?;
2088
2089 if args.dry_run {
2090 let preview = preview_rename(&manager, &args.pattern, &args.replacement)?;
2092
2093 if preview.is_empty() {
2094 println!("No variables match the pattern '{}'", args.pattern);
2095 } else {
2096 println!("Would rename {} variable(s):", preview.len());
2097
2098 let mut table = Table::new();
2099 table.load_preset(UTF8_FULL);
2100 table.set_header(vec!["Current Name", "New Name", "Value"]);
2101
2102 for (old, new, value) in preview {
2103 table.add_row(vec![old, new, value]);
2104 }
2105
2106 println!("{table}");
2107 println!("\nUse without --dry-run to apply changes");
2108 }
2109 } else {
2110 let renamed = manager.rename(&args.pattern, &args.replacement)?;
2111
2112 if renamed.is_empty() {
2113 println!("No variables match the pattern '{}'", args.pattern);
2114 } else {
2115 println!("ā
Renamed {} variable(s):", renamed.len());
2116
2117 let mut table = Table::new();
2118 table.load_preset(UTF8_FULL);
2119 table.set_header(vec!["Old Name", "New Name"]);
2120
2121 for (old, new) in &renamed {
2122 table.add_row(vec![old.clone(), new.clone()]);
2123 }
2124
2125 println!("{table}");
2126
2127 #[cfg(windows)]
2128 println!("\nš Note: You may need to restart your terminal for changes to take effect");
2129 }
2130 }
2131
2132 Ok(())
2133}
2134
2135fn preview_rename(manager: &EnvVarManager, pattern: &str, replacement: &str) -> Result<Vec<(String, String, String)>> {
2136 let mut preview = Vec::new();
2137
2138 if pattern.contains('*') {
2139 let (prefix, suffix) = split_wildcard_pattern(pattern)?;
2140 let (new_prefix, new_suffix) = split_wildcard_pattern(replacement)?;
2141
2142 for var in manager.list() {
2143 if var.name.starts_with(&prefix)
2144 && var.name.ends_with(&suffix)
2145 && var.name.len() >= prefix.len() + suffix.len()
2146 {
2147 let middle = &var.name[prefix.len()..var.name.len() - suffix.len()];
2148 let new_name = format!("{new_prefix}{middle}{new_suffix}");
2149 preview.push((var.name.clone(), new_name, var.value.clone()));
2150 }
2151 }
2152 } else if let Some(var) = manager.get(pattern) {
2153 preview.push((var.name.clone(), replacement.to_string(), var.value.clone()));
2154 }
2155
2156 Ok(preview)
2157}
2158
2159pub fn handle_replace(args: &ReplaceArgs) -> Result<()> {
2169 let mut manager = EnvVarManager::new();
2170 manager.load_all()?;
2171
2172 if args.dry_run {
2173 let preview = preview_replace(&manager, &args.pattern)?;
2175
2176 if preview.is_empty() {
2177 println!("No variables match the pattern '{}'", args.pattern);
2178 } else {
2179 println!("Would update {} variable(s):", preview.len());
2180
2181 let mut table = Table::new();
2182 table.load_preset(UTF8_FULL);
2183 table.set_header(vec!["Variable", "Current Value", "New Value"]);
2184
2185 for (name, current) in preview {
2186 table.add_row(vec![name, current, args.value.clone()]);
2187 }
2188
2189 println!("{table}");
2190 println!("\nUse without --dry-run to apply changes");
2191 }
2192 } else {
2193 let replaced = manager.replace(&args.pattern, &args.value)?;
2194
2195 if replaced.is_empty() {
2196 println!("No variables match the pattern '{}'", args.pattern);
2197 } else {
2198 println!("ā
Updated {} variable(s):", replaced.len());
2199
2200 let mut table = Table::new();
2201 table.load_preset(UTF8_FULL);
2202 table.set_header(vec!["Variable", "Old Value", "New Value"]);
2203
2204 for (name, old, new) in &replaced {
2205 let display_old = if old.len() > 50 {
2207 format!("{}...", &old[..47])
2208 } else {
2209 old.clone()
2210 };
2211 let display_new = if new.len() > 50 {
2212 format!("{}...", &new[..47])
2213 } else {
2214 new.clone()
2215 };
2216 table.add_row(vec![name.clone(), display_old, display_new]);
2217 }
2218
2219 println!("{table}");
2220
2221 #[cfg(windows)]
2222 println!("\nš Note: You may need to restart your terminal for changes to take effect");
2223 }
2224 }
2225
2226 Ok(())
2227}
2228
2229fn preview_replace(manager: &EnvVarManager, pattern: &str) -> Result<Vec<(String, String)>> {
2230 let mut preview = Vec::new();
2231
2232 if pattern.contains('*') {
2233 let (prefix, suffix) = split_wildcard_pattern(pattern)?;
2234
2235 for var in manager.list() {
2236 if var.name.starts_with(&prefix)
2237 && var.name.ends_with(&suffix)
2238 && var.name.len() >= prefix.len() + suffix.len()
2239 {
2240 preview.push((var.name.clone(), var.value.clone()));
2241 }
2242 }
2243 } else if let Some(var) = manager.get(pattern) {
2244 preview.push((var.name.clone(), var.value.clone()));
2245 }
2246
2247 Ok(preview)
2248}
2249
2250pub fn handle_find_replace(args: &FindReplaceArgs) -> Result<()> {
2261 let mut manager = EnvVarManager::new();
2262 manager.load_all()?;
2263
2264 if args.dry_run {
2265 let preview = preview_find_replace(&manager, &args.search, &args.replacement, args.pattern.as_deref())?;
2267
2268 if preview.is_empty() {
2269 println!("No variables contain '{}'", args.search);
2270 } else {
2271 println!("Would update {} variable(s):", preview.len());
2272
2273 let mut table = Table::new();
2274 table.load_preset(UTF8_FULL);
2275 table.set_header(vec!["Variable", "Current Value", "New Value"]);
2276
2277 for (name, old, new) in preview {
2278 table.add_row(vec![name, old, new]);
2279 }
2280
2281 println!("{table}");
2282 println!("\nUse without --dry-run to apply changes");
2283 }
2284 } else {
2285 let replaced = manager.find_replace(&args.search, &args.replacement, args.pattern.as_deref())?;
2286
2287 if replaced.is_empty() {
2288 println!("No variables contain '{}'", args.search);
2289 } else {
2290 println!("ā
Updated {} variable(s):", replaced.len());
2291
2292 let mut table = Table::new();
2293 table.load_preset(UTF8_FULL);
2294 table.set_header(vec!["Variable", "Old Value", "New Value"]);
2295
2296 for (name, old, new) in &replaced {
2297 let display_old = if old.len() > 50 {
2299 format!("{}...", &old[..47])
2300 } else {
2301 old.clone()
2302 };
2303 let display_new = if new.len() > 50 {
2304 format!("{}...", &new[..47])
2305 } else {
2306 new.clone()
2307 };
2308 table.add_row(vec![name.clone(), display_old, display_new]);
2309 }
2310
2311 println!("{table}");
2312
2313 #[cfg(windows)]
2314 println!("\nš Note: You may need to restart your terminal for changes to take effect");
2315 }
2316 }
2317
2318 Ok(())
2319}
2320
2321fn preview_find_replace(
2322 manager: &EnvVarManager,
2323 search: &str,
2324 replacement: &str,
2325 pattern: Option<&str>,
2326) -> Result<Vec<(String, String, String)>> {
2327 let mut preview = Vec::new();
2328
2329 for var in manager.list() {
2330 let matches_pattern = if let Some(pat) = pattern {
2332 if pat.contains('*') {
2333 let (prefix, suffix) = split_wildcard_pattern(pat)?;
2334 var.name.starts_with(&prefix)
2335 && var.name.ends_with(&suffix)
2336 && var.name.len() >= prefix.len() + suffix.len()
2337 } else {
2338 var.name == pat
2339 }
2340 } else {
2341 true
2342 };
2343
2344 if matches_pattern && var.value.contains(search) {
2345 let new_value = var.value.replace(search, replacement);
2346 preview.push((var.name.clone(), var.value.clone(), new_value));
2347 }
2348 }
2349
2350 Ok(preview)
2351}
2352
2353pub fn handle_watch(args: &WatchArgs) -> Result<()> {
2368 if matches!(args.direction, Direction::SystemToFile | Direction::Bidirectional) && args.output.is_none() {
2370 return Err(color_eyre::eyre::eyre!(
2371 "Output file required for system-to-file synchronization. Use --output <file>"
2372 ));
2373 }
2374
2375 let sync_mode = match args.direction {
2376 Direction::FileToSystem => SyncMode::FileToSystem,
2377 Direction::SystemToFile => SyncMode::SystemToFile,
2378 Direction::Bidirectional => SyncMode::Bidirectional,
2379 };
2380
2381 let mut config = WatchConfig {
2382 paths: if args.paths.is_empty() {
2383 vec![PathBuf::from(".")]
2384 } else {
2385 args.paths.clone()
2386 },
2387 mode: sync_mode,
2388 auto_reload: true,
2389 debounce_duration: Duration::from_millis(args.debounce),
2390 log_changes: !args.quiet,
2391 conflict_strategy: ConflictStrategy::UseLatest,
2392 ..Default::default()
2393 };
2394
2395 if !args.pattern.is_empty() {
2396 config.patterns.clone_from(&args.pattern);
2397 }
2398
2399 if let Some(output) = &args.output {
2401 if matches!(args.direction, Direction::Bidirectional) {
2402 config.paths.push(output.clone());
2403 }
2404 }
2405
2406 let mut manager = EnvVarManager::new();
2407 manager.load_all()?;
2408
2409 let mut watcher = EnvWatcher::new(config.clone(), manager);
2410
2411 if !args.vars.is_empty() {
2413 watcher.set_variable_filter(args.vars.clone());
2414 }
2415
2416 if let Some(output) = args.output.clone() {
2417 watcher.set_output_file(output);
2418 }
2419
2420 print_watch_header(args, &config);
2421
2422 watcher.start()?;
2423
2424 let running = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
2426 let r = running.clone();
2427
2428 ctrlc::set_handler(move || {
2429 r.store(false, std::sync::atomic::Ordering::SeqCst);
2430 })?;
2431
2432 while running.load(std::sync::atomic::Ordering::SeqCst) {
2434 std::thread::sleep(Duration::from_secs(1));
2435
2436 if let Some(log_file) = &args.log {
2437 let _ = watcher.export_change_log(log_file);
2438 }
2439 }
2440
2441 watcher.stop()?;
2442 println!("\nā
Watch mode stopped");
2443
2444 Ok(())
2445}
2446
2447fn print_watch_header(args: &WatchArgs, config: &WatchConfig) {
2448 println!("š Starting envx watch mode");
2449 println!("āāāāāāāāāāāāāāāāāāāāāāāāāāāā");
2450
2451 match args.direction {
2452 Direction::FileToSystem => {
2453 println!("š ā š» Syncing from files to system");
2454 println!(
2455 "Watching: {}",
2456 config
2457 .paths
2458 .iter()
2459 .map(|p| p.display().to_string())
2460 .collect::<Vec<_>>()
2461 .join(", ")
2462 );
2463 }
2464 Direction::SystemToFile => {
2465 println!("š» ā š Syncing from system to file");
2466 if let Some(output) = &args.output {
2467 println!("Output: {}", output.display());
2468 }
2469 }
2470 Direction::Bidirectional => {
2471 println!("š āļø š» Bidirectional sync");
2472 println!(
2473 "Watching: {}",
2474 config
2475 .paths
2476 .iter()
2477 .map(|p| p.display().to_string())
2478 .collect::<Vec<_>>()
2479 .join(", ")
2480 );
2481 if let Some(output) = &args.output {
2482 println!("Output: {}", output.display());
2483 }
2484 }
2485 }
2486
2487 if !args.vars.is_empty() {
2488 println!("Variables: {}", args.vars.join(", "));
2489 }
2490
2491 println!("āāāāāāāāāāāāāāāāāāāāāāāāāāāā");
2492 println!("Press Ctrl+C to stop\n");
2493}