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 comfy_table::presets::UTF8_FULL;
11use console::Term;
12use console::style;
13use envx_core::PathManager;
14use envx_core::ProjectConfig;
15use envx_core::ProjectManager;
16use envx_core::RequiredVar;
17use envx_core::ValidationReport;
18use envx_core::env::split_wildcard_pattern;
19use envx_core::profile_manager::ProfileManager;
20use envx_core::snapshot_manager::SnapshotManager;
21use envx_core::{Analyzer, EnvVarManager, ExportFormat, Exporter, ImportFormat, Importer};
22use std::io::Write;
23use std::path::Path;
24#[derive(Parser)]
25#[command(name = "envx")]
26#[command(about = "System Environment Variable Manager")]
27#[command(version)]
28pub struct Cli {
29 #[command(subcommand)]
30 pub command: Commands,
31}
32
33#[derive(Subcommand)]
34pub enum Commands {
35 List {
37 #[arg(short, long)]
39 source: Option<String>,
40
41 #[arg(short = 'q', long)]
43 query: Option<String>,
44
45 #[arg(short, long, default_value = "table")]
47 format: String,
48
49 #[arg(long, default_value = "name")]
51 sort: String,
52
53 #[arg(long)]
55 names_only: bool,
56
57 #[arg(short, long)]
59 limit: Option<usize>,
60
61 #[arg(long)]
63 stats: bool,
64 },
65
66 Get {
68 pattern: String,
77
78 #[arg(short, long, default_value = "simple")]
80 format: String,
81 },
82
83 Set {
85 name: String,
87
88 value: String,
90
91 #[arg(short, long)]
93 temporary: bool,
94 },
95
96 Delete {
98 pattern: String,
100
101 #[arg(short, long)]
103 force: bool,
104 },
105
106 Analyze {
108 #[arg(short, long, default_value = "all")]
110 analysis_type: String,
111 },
112
113 #[command(visible_alias = "ui")]
115 Tui,
116
117 Path {
119 #[command(subcommand)]
120 action: Option<PathAction>,
121
122 #[arg(short, long)]
124 check: bool,
125
126 #[arg(short = 'v', long, default_value = "PATH")]
128 var: String,
129
130 #[arg(short = 'p', long)]
132 permanent: bool,
133 },
134
135 Export {
137 file: String,
139
140 #[arg(short = 'v', long)]
142 vars: Vec<String>,
143
144 #[arg(short, long)]
146 format: Option<String>,
147
148 #[arg(short, long)]
150 source: Option<String>,
151
152 #[arg(short, long)]
154 metadata: bool,
155
156 #[arg(long)]
158 force: bool,
159 },
160
161 Import {
163 file: String,
165
166 #[arg(short = 'v', long)]
168 vars: Vec<String>,
169
170 #[arg(short, long)]
172 format: Option<String>,
173
174 #[arg(short, long)]
176 permanent: bool,
177
178 #[arg(long)]
180 prefix: Option<String>,
181
182 #[arg(long)]
184 overwrite: bool,
185
186 #[arg(short = 'n', long)]
188 dry_run: bool,
189 },
190
191 Snapshot(SnapshotArgs),
193
194 Profile(ProfileArgs),
196
197 Project(ProjectArgs),
199
200 Rename(RenameArgs),
202
203 Replace(ReplaceArgs),
205
206 FindReplace(FindReplaceArgs),
208}
209
210#[derive(Subcommand)]
211pub enum PathAction {
212 Add {
214 directory: String,
216
217 #[arg(short, long)]
219 first: bool,
220
221 #[arg(short, long)]
223 create: bool,
224 },
225
226 Remove {
228 directory: String,
230
231 #[arg(short, long)]
233 all: bool,
234 },
235
236 Clean {
238 #[arg(short, long)]
240 dedupe: bool,
241
242 #[arg(short = 'n', long)]
244 dry_run: bool,
245 },
246
247 Dedupe {
249 #[arg(short, long)]
251 keep_first: bool,
252
253 #[arg(short = 'n', long)]
255 dry_run: bool,
256 },
257
258 Check {
260 #[arg(short, long)]
262 verbose: bool,
263 },
264
265 List {
267 #[arg(short, long)]
269 numbered: bool,
270
271 #[arg(short, long)]
273 check: bool,
274 },
275
276 Move {
278 from: String,
280
281 to: String,
283 },
284}
285
286#[derive(Args)]
287pub struct SnapshotArgs {
288 #[command(subcommand)]
289 pub command: SnapshotCommands,
290}
291
292#[derive(Subcommand)]
293pub enum SnapshotCommands {
294 Create {
296 name: String,
298 #[arg(short, long)]
300 description: Option<String>,
301 },
302 List,
304 Show {
306 snapshot: String,
308 },
309 Restore {
311 snapshot: String,
313 #[arg(short, long)]
315 force: bool,
316 },
317 Delete {
319 snapshot: String,
321 #[arg(short, long)]
323 force: bool,
324 },
325 Diff {
327 snapshot1: String,
329 snapshot2: String,
331 },
332}
333
334#[derive(Args)]
335pub struct ProfileArgs {
336 #[command(subcommand)]
337 pub command: ProfileCommands,
338}
339
340#[derive(Subcommand)]
341pub enum ProfileCommands {
342 Create {
344 name: String,
346 #[arg(short, long)]
348 description: Option<String>,
349 },
350 List,
352 Show {
354 name: Option<String>,
356 },
357 Switch {
359 name: String,
361 #[arg(short, long)]
363 apply: bool,
364 },
365 Add {
367 profile: String,
369 name: String,
371 value: String,
373 #[arg(short, long)]
375 override_system: bool,
376 },
377 Remove {
379 profile: String,
381 name: String,
383 },
384 Delete {
386 name: String,
388 #[arg(short, long)]
390 force: bool,
391 },
392 Export {
394 name: String,
396 #[arg(short, long)]
398 output: Option<String>,
399 },
400 Import {
402 file: String,
404 #[arg(short, long)]
406 name: Option<String>,
407 #[arg(short, long)]
409 overwrite: bool,
410 },
411 Apply {
413 name: String,
415 },
416}
417
418#[derive(Args)]
419pub struct ProjectArgs {
420 #[command(subcommand)]
421 pub command: ProjectCommands,
422}
423
424#[derive(Subcommand)]
425pub enum ProjectCommands {
426 Init {
428 #[arg(short, long)]
430 name: Option<String>,
431 },
432 Apply {
434 #[arg(short, long)]
436 force: bool,
437 },
438 Check,
440 Edit,
442 Info,
444 Run {
446 script: String,
448 },
449 Require {
451 name: String,
453 #[arg(short, long)]
455 description: Option<String>,
456 #[arg(short, long)]
458 pattern: Option<String>,
459 #[arg(short, long)]
461 example: Option<String>,
462 },
463}
464
465#[derive(Args)]
466pub struct RenameArgs {
467 pub pattern: String,
469
470 pub replacement: String,
472
473 #[arg(long)]
475 pub dry_run: bool,
476}
477
478#[derive(Args)]
479pub struct ReplaceArgs {
480 pub pattern: String,
482
483 pub value: String,
485
486 #[arg(long)]
488 pub dry_run: bool,
489}
490
491#[derive(Args)]
492pub struct FindReplaceArgs {
493 pub search: String,
495
496 pub replacement: String,
498
499 #[arg(short = 'p', long)]
501 pub pattern: Option<String>,
502
503 #[arg(long)]
505 pub dry_run: bool,
506}
507
508pub fn execute(cli: Cli) -> Result<()> {
519 match cli.command {
520 Commands::List {
521 source,
522 query,
523 format,
524 sort,
525 names_only,
526 limit,
527 stats,
528 } => {
529 handle_list_command(
530 source.as_deref(),
531 query.as_deref(),
532 &format,
533 &sort,
534 names_only,
535 limit,
536 stats,
537 )?;
538 }
539
540 Commands::Get { pattern, format } => {
541 handle_get_command(&pattern, &format)?;
542 }
543
544 Commands::Set { name, value, temporary } => {
545 handle_set_command(&name, &value, temporary)?;
546 }
547
548 Commands::Delete { pattern, force } => {
549 handle_delete_command(&pattern, force)?;
550 }
551
552 Commands::Analyze { analysis_type } => {
553 handle_analyze_command(&analysis_type)?;
554 }
555
556 Commands::Tui => {
557 envx_tui::run()?;
559 }
560
561 Commands::Path {
562 action,
563 check,
564 var,
565 permanent,
566 } => {
567 handle_path_command(action, check, &var, permanent)?;
568 }
569
570 Commands::Export {
571 file,
572 vars,
573 format,
574 source,
575 metadata,
576 force,
577 } => {
578 handle_export(&file, &vars, format, source, metadata, force)?;
579 }
580
581 Commands::Import {
582 file,
583 vars,
584 format,
585 permanent,
586 prefix,
587 overwrite,
588 dry_run,
589 } => {
590 handle_import(&file, &vars, format, permanent, prefix.as_ref(), overwrite, dry_run)?;
591 }
592
593 Commands::Snapshot(args) => {
594 handle_snapshot(args)?;
595 }
596 Commands::Profile(args) => {
597 handle_profile(args)?;
598 }
599
600 Commands::Project(args) => {
601 handle_project(args)?;
602 }
603
604 Commands::Rename(args) => {
605 handle_rename(&args)?;
606 }
607
608 Commands::Replace(args) => {
609 handle_replace(&args)?;
610 }
611
612 Commands::FindReplace(args) => {
613 handle_find_replace(&args)?;
614 }
615 }
616
617 Ok(())
618}
619
620fn handle_get_command(pattern: &str, format: &str) -> Result<()> {
621 let mut manager = EnvVarManager::new();
622 manager.load_all()?;
623
624 let vars = manager.get_pattern(pattern);
625
626 if vars.is_empty() {
627 eprintln!("No variables found matching pattern: {pattern}");
628 return Ok(());
629 }
630
631 match format {
632 "json" => {
633 println!("{}", serde_json::to_string_pretty(&vars)?);
634 }
635 "detailed" => {
636 for var in vars {
637 println!("Name: {}", var.name);
638 println!("Value: {}", var.value);
639 println!("Source: {:?}", var.source);
640 println!("Modified: {}", var.modified.format("%Y-%m-%d %H:%M:%S"));
641 if let Some(orig) = &var.original_value {
642 println!("Original: {orig}");
643 }
644 println!("---");
645 }
646 }
647 _ => {
648 for var in vars {
649 println!("{} = {}", var.name, var.value);
650 }
651 }
652 }
653 Ok(())
654}
655
656fn handle_set_command(name: &str, value: &str, temporary: bool) -> Result<()> {
657 let mut manager = EnvVarManager::new();
658 manager.load_all()?;
659
660 let permanent = !temporary;
661
662 manager.set(name, value, permanent)?;
663 if permanent {
664 println!("✅ Set {name} = \"{value}\"");
665 #[cfg(windows)]
666 println!("📝 Note: You may need to restart your terminal for changes to take effect");
667 } else {
668 println!("⚡ Set {name} = \"{value}\" (temporary - current session only)");
669 }
670 Ok(())
671}
672
673fn handle_delete_command(pattern: &str, force: bool) -> Result<()> {
674 let mut manager = EnvVarManager::new();
675 manager.load_all()?;
676
677 let vars_to_delete: Vec<String> = manager
679 .get_pattern(pattern)
680 .into_iter()
681 .map(|v| v.name.clone())
682 .collect();
683
684 if vars_to_delete.is_empty() {
685 eprintln!("No variables found matching pattern: {pattern}");
686 return Ok(());
687 }
688
689 if !force && vars_to_delete.len() > 1 {
690 println!("About to delete {} variables:", vars_to_delete.len());
691 for name in &vars_to_delete {
692 println!(" - {name}");
693 }
694 print!("Continue? [y/N]: ");
695 std::io::stdout().flush()?;
696
697 let mut input = String::new();
698 std::io::stdin().read_line(&mut input)?;
699
700 if !input.trim().eq_ignore_ascii_case("y") {
701 println!("Cancelled.");
702 return Ok(());
703 }
704 }
705
706 for name in vars_to_delete {
708 manager.delete(&name)?;
709 println!("Deleted: {name}");
710 }
711 Ok(())
712}
713
714fn handle_analyze_command(analysis_type: &str) -> Result<()> {
715 let mut manager = EnvVarManager::new();
716 manager.load_all()?;
717 let vars = manager.list().into_iter().cloned().collect();
718 let analyzer = Analyzer::new(vars);
719
720 match analysis_type {
721 "duplicates" | "all" => {
722 let duplicates = analyzer.find_duplicates();
723 if !duplicates.is_empty() {
724 println!("Duplicate variables found:");
725 for (name, vars) in duplicates {
726 println!(" {}: {} instances", name, vars.len());
727 }
728 }
729 }
730 "invalid" => {
731 let validation = analyzer.validate_all();
732 for (name, result) in validation {
733 if !result.valid {
734 println!("Invalid variable: {name}");
735 for error in result.errors {
736 println!(" Error: {error}");
737 }
738 }
739 }
740 }
741 _ => {}
742 }
743 Ok(())
744}
745
746#[allow(clippy::too_many_lines)]
747fn handle_path_command(action: Option<PathAction>, check: bool, var: &str, permanent: bool) -> Result<()> {
748 let mut manager = EnvVarManager::new();
749 manager.load_all()?;
750
751 let path_var = manager.get(var).ok_or_else(|| eyre!("Variable '{}' not found", var))?;
753
754 let mut path_mgr = PathManager::new(&path_var.value);
755
756 if action.is_none() {
758 if check {
759 handle_path_check(&path_mgr, true);
760 }
761 handle_path_list(&path_mgr, false, false);
762 }
763
764 let command = action.expect("Action should be Some if we reach here");
765 match command {
766 PathAction::Add {
767 directory,
768 first,
769 create,
770 } => {
771 let path = Path::new(&directory);
772
773 if !path.exists() {
775 if create {
776 std::fs::create_dir_all(path)?;
777 println!("Created directory: {directory}");
778 } else if !path.exists() {
779 eprintln!("Warning: Directory does not exist: {directory}");
780 print!("Add anyway? [y/N]: ");
781 std::io::stdout().flush()?;
782
783 let mut input = String::new();
784 std::io::stdin().read_line(&mut input)?;
785
786 if !input.trim().eq_ignore_ascii_case("y") {
787 return Ok(());
788 }
789 }
790 }
791
792 if path_mgr.contains(&directory) {
794 println!("Directory already in {var}: {directory}");
795 return Ok(());
796 }
797
798 if first {
800 path_mgr.add_first(directory.clone());
801 println!("Added to beginning of {var}: {directory}");
802 } else {
803 path_mgr.add_last(directory.clone());
804 println!("Added to end of {var}: {directory}");
805 }
806
807 let new_value = path_mgr.to_string();
809 manager.set(var, &new_value, permanent)?;
810 }
811
812 PathAction::Remove { directory, all } => {
813 let removed = if all {
814 path_mgr.remove_all(&directory)
815 } else {
816 path_mgr.remove_first(&directory)
817 };
818
819 if removed > 0 {
820 println!("Removed {removed} occurrence(s) of: {directory}");
821 let new_value = path_mgr.to_string();
822 manager.set(var, &new_value, permanent)?;
823 } else {
824 println!("Directory not found in {var}: {directory}");
825 }
826 }
827
828 PathAction::Clean { dedupe, dry_run } => {
829 let invalid = path_mgr.get_invalid();
830 let duplicates = if dedupe { path_mgr.get_duplicates() } else { vec![] };
831
832 if invalid.is_empty() && duplicates.is_empty() {
833 println!("No invalid or duplicate entries found in {var}");
834 return Ok(());
835 }
836
837 if !invalid.is_empty() {
838 println!("Invalid/non-existent paths to remove:");
839 for path in &invalid {
840 println!(" - {path}");
841 }
842 }
843
844 if !duplicates.is_empty() {
845 println!("Duplicate paths to remove:");
846 for path in &duplicates {
847 println!(" - {path}");
848 }
849 }
850
851 if dry_run {
852 println!("\n(Dry run - no changes made)");
853 } else {
854 let removed_invalid = path_mgr.remove_invalid();
855 let removed_dupes = if dedupe {
856 path_mgr.deduplicate(false) } else {
858 0
859 };
860
861 println!("Removed {removed_invalid} invalid and {removed_dupes} duplicate entries");
862 let new_value = path_mgr.to_string();
863 manager.set(var, &new_value, permanent)?;
864 }
865 }
866
867 PathAction::Dedupe { keep_first, dry_run } => {
868 let duplicates = path_mgr.get_duplicates();
869
870 if duplicates.is_empty() {
871 println!("No duplicate entries found in {var}");
872 return Ok(());
873 }
874
875 println!("Duplicate paths to remove:");
876 for path in &duplicates {
877 println!(" - {path}");
878 }
879 println!(
880 "Strategy: keep {} occurrence",
881 if keep_first { "first" } else { "last" }
882 );
883
884 if dry_run {
885 println!("\n(Dry run - no changes made)");
886 } else {
887 let removed = path_mgr.deduplicate(keep_first);
888 println!("Removed {removed} duplicate entries");
889 let new_value = path_mgr.to_string();
890 manager.set(var, &new_value, permanent)?;
891 }
892 }
893
894 PathAction::Check { verbose } => {
895 handle_path_check(&path_mgr, verbose);
896 }
897
898 PathAction::List { numbered, check } => {
899 handle_path_list(&path_mgr, numbered, check);
900 }
901
902 PathAction::Move { from, to } => {
903 let from_idx = if let Ok(idx) = from.parse::<usize>() {
905 idx
906 } else {
907 path_mgr
908 .find_index(&from)
909 .ok_or_else(|| eyre!("Path not found: {}", from))?
910 };
911
912 let to_idx = match to.as_str() {
914 "first" => 0,
915 "last" => path_mgr.len() - 1,
916 _ => to.parse::<usize>().map_err(|_| eyre!("Invalid position: {}", to))?,
917 };
918
919 path_mgr.move_entry(from_idx, to_idx)?;
920 println!("Moved entry from position {from_idx} to {to_idx}");
921
922 let new_value = path_mgr.to_string();
923 manager.set(var, &new_value, permanent)?;
924 }
925 }
926
927 Ok(())
928}
929
930fn handle_path_check(path_mgr: &PathManager, verbose: bool) {
931 let entries = path_mgr.entries();
932 let mut issues = Vec::new();
933 let mut valid_count = 0;
934
935 for (idx, entry) in entries.iter().enumerate() {
936 let path = Path::new(entry);
937 let exists = path.exists();
938 let is_dir = path.is_dir();
939
940 if verbose || !exists {
941 let status = if !exists {
942 issues.push(format!("Not found: {entry}"));
943 "❌ NOT FOUND"
944 } else if !is_dir {
945 issues.push(format!("Not a directory: {entry}"));
946 "⚠️ NOT DIR"
947 } else {
948 valid_count += 1;
949 "✓ OK"
950 };
951
952 if verbose {
953 println!("[{idx:3}] {status} - {entry}");
954 }
955 } else if exists && is_dir {
956 valid_count += 1;
957 }
958 }
959
960 println!("\nPATH Analysis:");
962 println!(" Total entries: {}", entries.len());
963 println!(" Valid entries: {valid_count}");
964
965 let duplicates = path_mgr.get_duplicates();
966 if !duplicates.is_empty() {
967 println!(" Duplicates: {} entries", duplicates.len());
968 if verbose {
969 for dup in &duplicates {
970 println!(" - {dup}");
971 }
972 }
973 }
974
975 let invalid = path_mgr.get_invalid();
976 if !invalid.is_empty() {
977 println!(" Invalid entries: {}", invalid.len());
978 if verbose {
979 for inv in &invalid {
980 println!(" - {inv}");
981 }
982 }
983 }
984
985 if issues.is_empty() {
986 println!("\n✅ No issues found!");
987 } else {
988 println!("\n⚠️ {} issue(s) found", issues.len());
989 if !verbose {
990 println!("Run with --verbose for details");
991 }
992 }
993}
994
995fn handle_path_list(path_mgr: &PathManager, numbered: bool, check: bool) {
996 let entries = path_mgr.entries();
997
998 if entries.is_empty() {
999 println!("PATH is empty");
1000 }
1001
1002 for (idx, entry) in entries.iter().enumerate() {
1003 let prefix = if numbered { format!("[{idx:3}] ") } else { String::new() };
1004
1005 let suffix = if check {
1006 let path = Path::new(entry);
1007 if !path.exists() {
1008 " [NOT FOUND]"
1009 } else if !path.is_dir() {
1010 " [NOT A DIRECTORY]"
1011 } else {
1012 ""
1013 }
1014 } else {
1015 ""
1016 };
1017
1018 println!("{prefix}{entry}{suffix}");
1019 }
1020}
1021
1022fn handle_export(
1023 file: &str,
1024 vars: &[String],
1025 format: Option<String>,
1026 source: Option<String>,
1027 metadata: bool,
1028 force: bool,
1029) -> Result<()> {
1030 if Path::new(&file).exists() && !force {
1032 print!("File '{file}' already exists. Overwrite? [y/N]: ");
1033 std::io::stdout().flush()?;
1034
1035 let mut input = String::new();
1036 std::io::stdin().read_line(&mut input)?;
1037
1038 if !input.trim().eq_ignore_ascii_case("y") {
1039 println!("Export cancelled.");
1040 return Ok(());
1041 }
1042 }
1043
1044 let mut manager = EnvVarManager::new();
1046 manager.load_all()?;
1047
1048 let mut vars_to_export = if vars.is_empty() {
1050 manager.list().into_iter().cloned().collect()
1051 } else {
1052 let mut selected = Vec::new();
1053 for pattern in vars {
1054 let matched = manager.get_pattern(pattern);
1055 selected.extend(matched.into_iter().cloned());
1056 }
1057 selected
1058 };
1059
1060 if let Some(src) = source {
1062 let source_filter = match src.as_str() {
1063 "system" => envx_core::EnvVarSource::System,
1064 "user" => envx_core::EnvVarSource::User,
1065 "process" => envx_core::EnvVarSource::Process,
1066 "shell" => envx_core::EnvVarSource::Shell,
1067 _ => return Err(eyre!("Invalid source: {}", src)),
1068 };
1069
1070 vars_to_export.retain(|v| v.source == source_filter);
1071 }
1072
1073 if vars_to_export.is_empty() {
1074 println!("No variables to export.");
1075 return Ok(());
1076 }
1077
1078 let export_format = if let Some(fmt) = format {
1080 match fmt.as_str() {
1081 "env" => ExportFormat::DotEnv,
1082 "json" => ExportFormat::Json,
1083 "yaml" | "yml" => ExportFormat::Yaml,
1084 "txt" | "text" => ExportFormat::Text,
1085 "ps1" | "powershell" => ExportFormat::PowerShell,
1086 "sh" | "bash" => ExportFormat::Shell,
1087 _ => return Err(eyre!("Unsupported format: {}", fmt)),
1088 }
1089 } else {
1090 ExportFormat::from_extension(file)?
1092 };
1093
1094 let exporter = Exporter::new(vars_to_export, metadata);
1096 exporter.export_to_file(file, export_format)?;
1097
1098 println!("Exported {} variables to '{}'", exporter.count(), file);
1099
1100 Ok(())
1101}
1102
1103fn handle_import(
1104 file: &str,
1105 vars: &[String],
1106 format: Option<String>,
1107 permanent: bool,
1108 prefix: Option<&String>,
1109 overwrite: bool,
1110 dry_run: bool,
1111) -> Result<()> {
1112 if !Path::new(&file).exists() {
1114 return Err(eyre!("File not found: {}", file));
1115 }
1116
1117 let import_format = if let Some(fmt) = format {
1119 match fmt.as_str() {
1120 "env" => ImportFormat::DotEnv,
1121 "json" => ImportFormat::Json,
1122 "yaml" | "yml" => ImportFormat::Yaml,
1123 "txt" | "text" => ImportFormat::Text,
1124 _ => return Err(eyre!("Unsupported format: {}", fmt)),
1125 }
1126 } else {
1127 ImportFormat::from_extension(file)?
1129 };
1130
1131 let mut importer = Importer::new();
1133 importer.import_from_file(file, import_format)?;
1134
1135 if !vars.is_empty() {
1137 importer.filter_by_patterns(vars);
1138 }
1139
1140 if let Some(pfx) = &prefix {
1142 importer.add_prefix(pfx);
1143 }
1144
1145 let import_vars = importer.get_variables();
1147
1148 if import_vars.is_empty() {
1149 println!("No variables to import.");
1150 return Ok(());
1151 }
1152
1153 let mut manager = EnvVarManager::new();
1155 manager.load_all()?;
1156
1157 let mut conflicts = Vec::new();
1158 for (name, _) in &import_vars {
1159 if manager.get(name).is_some() {
1160 conflicts.push(name.clone());
1161 }
1162 }
1163
1164 if !conflicts.is_empty() && !overwrite && !dry_run {
1165 println!("The following variables already exist:");
1166 for name in &conflicts {
1167 println!(" - {name}");
1168 }
1169
1170 print!("Overwrite existing variables? [y/N]: ");
1171 std::io::stdout().flush()?;
1172
1173 let mut input = String::new();
1174 std::io::stdin().read_line(&mut input)?;
1175
1176 if !input.trim().eq_ignore_ascii_case("y") {
1177 println!("Import cancelled.");
1178 return Ok(());
1179 }
1180 }
1181
1182 if dry_run {
1184 println!("Would import {} variables:", import_vars.len());
1185 for (name, value) in &import_vars {
1186 let status = if conflicts.contains(name) {
1187 " [OVERWRITE]"
1188 } else {
1189 " [NEW]"
1190 };
1191 println!(
1192 " {} = {}{}",
1193 name,
1194 if value.len() > 50 {
1195 format!("{}...", &value[..50])
1196 } else {
1197 value.clone()
1198 },
1199 status
1200 );
1201 }
1202 println!("\n(Dry run - no changes made)");
1203 } else {
1204 let mut imported = 0;
1206 let mut failed = 0;
1207
1208 for (name, value) in import_vars {
1209 match manager.set(&name, &value, permanent) {
1210 Ok(()) => imported += 1,
1211 Err(e) => {
1212 eprintln!("Failed to import {name}: {e}");
1213 failed += 1;
1214 }
1215 }
1216 }
1217
1218 println!("Imported {imported} variables");
1219 if failed > 0 {
1220 println!("Failed to import {failed} variables");
1221 }
1222 }
1223
1224 Ok(())
1225}
1226
1227fn handle_list_command(
1228 source: Option<&str>,
1229 query: Option<&str>,
1230 format: &str,
1231 sort: &str,
1232 names_only: bool,
1233 limit: Option<usize>,
1234 stats: bool,
1235) -> Result<()> {
1236 let mut manager = EnvVarManager::new();
1237 manager.load_all()?;
1238
1239 let mut vars = if let Some(q) = &query {
1241 manager.search(q)
1242 } else if let Some(src) = source {
1243 let source_filter = match src {
1244 "system" => envx_core::EnvVarSource::System,
1245 "user" => envx_core::EnvVarSource::User,
1246 "process" => envx_core::EnvVarSource::Process,
1247 "shell" => envx_core::EnvVarSource::Shell,
1248 _ => return Err(eyre!("Invalid source: {}", src)),
1249 };
1250 manager.filter_by_source(&source_filter)
1251 } else {
1252 manager.list()
1253 };
1254
1255 match sort {
1257 "name" => vars.sort_by(|a, b| a.name.cmp(&b.name)),
1258 "value" => vars.sort_by(|a, b| a.value.cmp(&b.value)),
1259 "source" => vars.sort_by(|a, b| format!("{:?}", a.source).cmp(&format!("{:?}", b.source))),
1260 _ => {}
1261 }
1262
1263 let total_count = vars.len();
1265 if let Some(lim) = limit {
1266 vars.truncate(lim);
1267 }
1268
1269 if stats || (format == "table" && !names_only) {
1271 print_statistics(&manager, &vars, total_count, query, source);
1272 }
1273
1274 if names_only {
1276 for var in vars {
1277 println!("{}", var.name);
1278 }
1279 return Ok(());
1280 }
1281
1282 match format {
1284 "json" => {
1285 println!("{}", serde_json::to_string_pretty(&vars)?);
1286 }
1287 "simple" => {
1288 for var in vars {
1289 println!("{} = {}", style(&var.name).cyan(), var.value);
1290 }
1291 }
1292 "compact" => {
1293 for var in vars {
1294 let source_str = format_source_compact(&var.source);
1295 println!(
1296 "{} {} = {}",
1297 source_str,
1298 style(&var.name).bright(),
1299 style(truncate_value(&var.value, 60)).dim()
1300 );
1301 }
1302 }
1303 _ => {
1304 print_table(vars, limit.is_some());
1305 }
1306 }
1307
1308 if let Some(lim) = limit {
1310 if total_count > lim {
1311 println!(
1312 "\n{}",
1313 style(format!(
1314 "Showing {lim} of {total_count} total variables. Use --limit to see more."
1315 ))
1316 .yellow()
1317 );
1318 }
1319 }
1320
1321 Ok(())
1322}
1323
1324pub fn handle_snapshot(args: SnapshotArgs) -> Result<()> {
1336 let snapshot_manager = SnapshotManager::new()?;
1337 let mut env_manager = EnvVarManager::new();
1338 env_manager.load_all()?;
1339
1340 match args.command {
1341 SnapshotCommands::Create { name, description } => {
1342 let vars = env_manager.list().into_iter().cloned().collect();
1343 let snapshot = snapshot_manager.create(name, description, vars)?;
1344 println!("✅ Created snapshot: {} (ID: {})", snapshot.name, snapshot.id);
1345 }
1346 SnapshotCommands::List => {
1347 let snapshots = snapshot_manager.list()?;
1348 if snapshots.is_empty() {
1349 println!("No snapshots found.");
1350 return Ok(());
1351 }
1352
1353 let mut table = Table::new();
1354 table.set_header(vec!["Name", "ID", "Created", "Variables", "Description"]);
1355
1356 for snapshot in snapshots {
1357 table.add_row(vec![
1358 snapshot.name,
1359 snapshot.id[..8].to_string(),
1360 snapshot.created_at.format("%Y-%m-%d %H:%M").to_string(),
1361 snapshot.variables.len().to_string(),
1362 snapshot.description.unwrap_or_default(),
1363 ]);
1364 }
1365
1366 println!("{table}");
1367 }
1368 SnapshotCommands::Show { snapshot } => {
1369 let snap = snapshot_manager.get(&snapshot)?;
1370 println!("Snapshot: {}", snap.name);
1371 println!("ID: {}", snap.id);
1372 println!("Created: {}", snap.created_at.format("%Y-%m-%d %H:%M:%S"));
1373 println!("Description: {}", snap.description.unwrap_or_default());
1374 println!("Variables: {}", snap.variables.len());
1375
1376 println!("\nFirst 10 variables:");
1378 for (i, (name, var)) in snap.variables.iter().take(10).enumerate() {
1379 println!(" {}. {} = {}", i + 1, name, var.value);
1380 }
1381
1382 if snap.variables.len() > 10 {
1383 println!(" ... and {} more", snap.variables.len() - 10);
1384 }
1385 }
1386 SnapshotCommands::Restore { snapshot, force } => {
1387 if !force {
1388 print!("⚠️ This will replace all current environment variables. Continue? [y/N] ");
1389 std::io::Write::flush(&mut std::io::stdout())?;
1390
1391 let mut input = String::new();
1392 std::io::stdin().read_line(&mut input)?;
1393 if !input.trim().eq_ignore_ascii_case("y") {
1394 println!("Cancelled.");
1395 return Ok(());
1396 }
1397 }
1398
1399 snapshot_manager.restore(&snapshot, &mut env_manager)?;
1400 println!("✅ Restored from snapshot: {snapshot}");
1401 }
1402 SnapshotCommands::Delete { snapshot, force } => {
1403 if !force {
1404 print!("⚠️ Delete snapshot '{snapshot}'? [y/N] ");
1405 std::io::Write::flush(&mut std::io::stdout())?;
1406
1407 let mut input = String::new();
1408 std::io::stdin().read_line(&mut input)?;
1409 if !input.trim().eq_ignore_ascii_case("y") {
1410 println!("Cancelled.");
1411 return Ok(());
1412 }
1413 }
1414
1415 snapshot_manager.delete(&snapshot)?;
1416 println!("✅ Deleted snapshot: {snapshot}");
1417 }
1418 SnapshotCommands::Diff { snapshot1, snapshot2 } => {
1419 let diff = snapshot_manager.diff(&snapshot1, &snapshot2)?;
1420
1421 if diff.added.is_empty() && diff.removed.is_empty() && diff.modified.is_empty() {
1422 println!("No differences found between snapshots.");
1423 return Ok(());
1424 }
1425
1426 if !diff.added.is_empty() {
1427 println!("➕ Added in {snapshot2}:");
1428 for (name, var) in &diff.added {
1429 println!(" {} = {}", name, var.value);
1430 }
1431 }
1432
1433 if !diff.removed.is_empty() {
1434 println!("\n➖ Removed in {snapshot2}:");
1435 for (name, var) in &diff.removed {
1436 println!(" {} = {}", name, var.value);
1437 }
1438 }
1439
1440 if !diff.modified.is_empty() {
1441 println!("\n🔄 Modified:");
1442 for (name, (old, new)) in &diff.modified {
1443 println!(" {name}:");
1444 println!(" Old: {}", old.value);
1445 println!(" New: {}", new.value);
1446 }
1447 }
1448 }
1449 }
1450
1451 Ok(())
1452}
1453
1454pub fn handle_profile(args: ProfileArgs) -> Result<()> {
1467 let mut profile_manager = ProfileManager::new()?;
1468 let mut env_manager = EnvVarManager::new();
1469 env_manager.load_all()?;
1470
1471 match args.command {
1472 ProfileCommands::Create { name, description } => {
1473 handle_profile_create(&mut profile_manager, &name, description)?;
1474 }
1475 ProfileCommands::List => {
1476 handle_profile_list(&profile_manager);
1477 }
1478 ProfileCommands::Show { name } => {
1479 handle_profile_show(&profile_manager, name)?;
1480 }
1481 ProfileCommands::Switch { name, apply } => {
1482 handle_profile_switch(&mut profile_manager, &mut env_manager, &name, apply)?;
1483 }
1484 ProfileCommands::Add {
1485 profile,
1486 name,
1487 value,
1488 override_system,
1489 } => {
1490 handle_profile_add(&mut profile_manager, &profile, &name, &value, override_system)?;
1491 }
1492 ProfileCommands::Remove { profile, name } => {
1493 handle_profile_remove(&mut profile_manager, &profile, &name)?;
1494 }
1495 ProfileCommands::Delete { name, force } => {
1496 handle_profile_delete(&mut profile_manager, &name, force)?;
1497 }
1498 ProfileCommands::Export { name, output } => {
1499 handle_profile_export(&profile_manager, &name, output)?;
1500 }
1501 ProfileCommands::Import { file, name, overwrite } => {
1502 handle_profile_import(&mut profile_manager, &file, name, overwrite)?;
1503 }
1504 ProfileCommands::Apply { name } => {
1505 handle_profile_apply(&mut profile_manager, &mut env_manager, &name)?;
1506 }
1507 }
1508
1509 Ok(())
1510}
1511
1512fn handle_profile_create(profile_manager: &mut ProfileManager, name: &str, description: Option<String>) -> Result<()> {
1513 profile_manager.create(name.to_string(), description)?;
1514 println!("✅ Created profile: {name}");
1515 Ok(())
1516}
1517
1518fn handle_profile_list(profile_manager: &ProfileManager) {
1519 let profiles = profile_manager.list();
1520 if profiles.is_empty() {
1521 println!("No profiles found.");
1522 }
1523
1524 let active = profile_manager.active().map(|p| &p.name);
1525 let mut table = Table::new();
1526 table.set_header(vec!["Name", "Variables", "Created", "Description", "Status"]);
1527
1528 for profile in profiles {
1529 let status = if active == Some(&profile.name) {
1530 "● Active"
1531 } else {
1532 ""
1533 };
1534
1535 table.add_row(vec![
1536 profile.name.clone(),
1537 profile.variables.len().to_string(),
1538 profile.created_at.format("%Y-%m-%d").to_string(),
1539 profile.description.clone().unwrap_or_default(),
1540 status.to_string(),
1541 ]);
1542 }
1543
1544 println!("{table}");
1545}
1546
1547fn handle_profile_show(profile_manager: &ProfileManager, name: Option<String>) -> Result<()> {
1548 let profile = if let Some(name) = name {
1549 profile_manager
1550 .get(&name)
1551 .ok_or_else(|| color_eyre::eyre::eyre!("Profile '{}' not found", name))?
1552 } else {
1553 profile_manager
1554 .active()
1555 .ok_or_else(|| color_eyre::eyre::eyre!("No active profile"))?
1556 };
1557
1558 println!("Profile: {}", profile.name);
1559 println!("Description: {}", profile.description.as_deref().unwrap_or(""));
1560 println!("Created: {}", profile.created_at.format("%Y-%m-%d %H:%M:%S"));
1561 println!("Updated: {}", profile.updated_at.format("%Y-%m-%d %H:%M:%S"));
1562 if let Some(parent) = &profile.parent {
1563 println!("Inherits from: {parent}");
1564 }
1565 println!("\nVariables:");
1566
1567 for (name, var) in &profile.variables {
1568 let status = if var.enabled { "✓" } else { "✗" };
1569 let override_flag = if var.override_system { " [override]" } else { "" };
1570 println!(" {} {} = {}{}", status, name, var.value, override_flag);
1571 }
1572 Ok(())
1573}
1574
1575fn handle_profile_switch(
1576 profile_manager: &mut ProfileManager,
1577 env_manager: &mut EnvVarManager,
1578 name: &str,
1579 apply: bool,
1580) -> Result<()> {
1581 profile_manager.switch(name)?;
1582 println!("✅ Switched to profile: {name}");
1583
1584 if apply {
1585 profile_manager.apply(name, env_manager)?;
1586 println!("✅ Applied profile variables");
1587 }
1588 Ok(())
1589}
1590
1591fn handle_profile_add(
1592 profile_manager: &mut ProfileManager,
1593 profile: &str,
1594 name: &str,
1595 value: &str,
1596 override_system: bool,
1597) -> Result<()> {
1598 let prof = profile_manager
1599 .get_mut(profile)
1600 .ok_or_else(|| color_eyre::eyre::eyre!("Profile '{}' not found", profile))?;
1601
1602 prof.add_var(name.to_string(), value.to_string(), override_system);
1603 profile_manager.save()?;
1604
1605 println!("✅ Added {name} to profile {profile}");
1606 Ok(())
1607}
1608
1609fn handle_profile_remove(profile_manager: &mut ProfileManager, profile: &str, name: &str) -> Result<()> {
1610 let prof = profile_manager
1611 .get_mut(profile)
1612 .ok_or_else(|| color_eyre::eyre::eyre!("Profile '{}' not found", profile))?;
1613
1614 prof.remove_var(name)
1615 .ok_or_else(|| color_eyre::eyre::eyre!("Variable '{}' not found in profile", name))?;
1616
1617 profile_manager.save()?;
1618 println!("✅ Removed {name} from profile {profile}");
1619 Ok(())
1620}
1621
1622fn handle_profile_delete(profile_manager: &mut ProfileManager, name: &str, force: bool) -> Result<()> {
1623 if !force {
1624 print!("⚠️ Delete profile '{name}'? [y/N] ");
1625 std::io::Write::flush(&mut std::io::stdout())?;
1626
1627 let mut input = String::new();
1628 std::io::stdin().read_line(&mut input)?;
1629 if !input.trim().eq_ignore_ascii_case("y") {
1630 println!("Cancelled.");
1631 return Ok(());
1632 }
1633 }
1634
1635 profile_manager.delete(name)?;
1636 println!("✅ Deleted profile: {name}");
1637 Ok(())
1638}
1639
1640fn handle_profile_export(profile_manager: &ProfileManager, name: &str, output: Option<String>) -> Result<()> {
1641 let json = profile_manager.export(name)?;
1642
1643 if let Some(path) = output {
1644 std::fs::write(path, json)?;
1645 println!("✅ Exported profile to file");
1646 } else {
1647 println!("{json}");
1648 }
1649 Ok(())
1650}
1651
1652fn handle_profile_import(
1653 profile_manager: &mut ProfileManager,
1654 file: &str,
1655 name: Option<String>,
1656 overwrite: bool,
1657) -> Result<()> {
1658 let content = std::fs::read_to_string(file)?;
1659 let import_name = name.unwrap_or_else(|| "imported".to_string());
1660
1661 profile_manager.import(import_name.clone(), &content, overwrite)?;
1662 println!("✅ Imported profile: {import_name}");
1663 Ok(())
1664}
1665
1666fn handle_profile_apply(
1667 profile_manager: &mut ProfileManager,
1668 env_manager: &mut EnvVarManager,
1669 name: &str,
1670) -> Result<()> {
1671 profile_manager.apply(name, env_manager)?;
1672 println!("✅ Applied profile: {name}");
1673 Ok(())
1674}
1675
1676fn print_statistics(
1677 manager: &EnvVarManager,
1678 filtered_vars: &[&envx_core::EnvVar],
1679 total_count: usize,
1680 query: Option<&str>,
1681 source: Option<&str>,
1682) {
1683 let _term = Term::stdout();
1684
1685 let system_count = manager.filter_by_source(&envx_core::EnvVarSource::System).len();
1687 let user_count = manager.filter_by_source(&envx_core::EnvVarSource::User).len();
1688 let process_count = manager.filter_by_source(&envx_core::EnvVarSource::Process).len();
1689 let shell_count = manager.filter_by_source(&envx_core::EnvVarSource::Shell).len();
1690
1691 println!("{}", style("═".repeat(60)).blue().bold());
1693 println!("{}", style("Environment Variables Summary").cyan().bold());
1694 println!("{}", style("═".repeat(60)).blue().bold());
1695
1696 if query.is_some() || source.is_some() {
1698 print!(" {} ", style("Filter:").yellow());
1699 if let Some(q) = query {
1700 print!("query='{}' ", style(q).green());
1701 }
1702 if let Some(s) = source {
1703 print!("source={} ", style(s).green());
1704 }
1705 println!();
1706 println!(
1707 " {} {}/{} variables",
1708 style("Showing:").yellow(),
1709 style(filtered_vars.len()).green().bold(),
1710 total_count
1711 );
1712 } else {
1713 println!(
1714 " {} {} variables",
1715 style("Total:").yellow(),
1716 style(total_count).green().bold()
1717 );
1718 }
1719
1720 println!();
1721 println!(" {} By Source:", style("►").cyan());
1722
1723 let max_count = system_count.max(user_count).max(process_count).max(shell_count);
1725 let bar_width = 30;
1726
1727 print_source_bar("System", system_count, max_count, bar_width, "red");
1728 print_source_bar("User", user_count, max_count, bar_width, "yellow");
1729 print_source_bar("Process", process_count, max_count, bar_width, "green");
1730 print_source_bar("Shell", shell_count, max_count, bar_width, "cyan");
1731
1732 println!("{}", style("─".repeat(60)).blue());
1733 println!();
1734}
1735
1736fn print_source_bar(label: &str, count: usize, max: usize, width: usize, color: &str) {
1737 let filled = if max > 0 { (count * width / max).max(1) } else { 0 };
1738
1739 let bar = "█".repeat(filled);
1740 let empty = "░".repeat(width - filled);
1741
1742 let colored_bar = match color {
1743 "red" => style(bar).red(),
1744 "yellow" => style(bar).yellow(),
1745 "green" => style(bar).green(),
1746 "cyan" => style(bar).cyan(),
1747 _ => style(bar).white(),
1748 };
1749
1750 println!(
1751 " {:10} {} {}{} {}",
1752 style(label).bold(),
1753 colored_bar,
1754 style(empty).dim(),
1755 style(format!(" {count:4}")).bold(),
1756 style("vars").dim()
1757 );
1758}
1759
1760fn print_table(vars: Vec<&envx_core::EnvVar>, _is_limited: bool) {
1761 if vars.is_empty() {
1762 println!("{}", style("No environment variables found.").yellow());
1763 }
1764
1765 let mut table = Table::new();
1766
1767 table
1769 .set_content_arrangement(ContentArrangement::Dynamic)
1770 .set_width(120)
1771 .set_header(vec![
1772 Cell::new("Source").add_attribute(Attribute::Bold).fg(Color::Cyan),
1773 Cell::new("Name").add_attribute(Attribute::Bold).fg(Color::Cyan),
1774 Cell::new("Value").add_attribute(Attribute::Bold).fg(Color::Cyan),
1775 ]);
1776
1777 for var in vars {
1779 let (source_str, source_color) = format_source(&var.source);
1780 let truncated_value = truncate_value(&var.value, 50);
1781
1782 table.add_row(vec![
1783 Cell::new(source_str).fg(source_color),
1784 Cell::new(&var.name).fg(Color::White),
1785 Cell::new(truncated_value).fg(Color::Grey),
1786 ]);
1787 }
1788
1789 println!("{table}");
1790}
1791
1792fn format_source(source: &envx_core::EnvVarSource) -> (String, Color) {
1793 match source {
1794 envx_core::EnvVarSource::System => ("System".to_string(), Color::Red),
1795 envx_core::EnvVarSource::User => ("User".to_string(), Color::Yellow),
1796 envx_core::EnvVarSource::Process => ("Process".to_string(), Color::Green),
1797 envx_core::EnvVarSource::Shell => ("Shell".to_string(), Color::Cyan),
1798 envx_core::EnvVarSource::Application(app) => (format!("App:{app}"), Color::Magenta),
1799 }
1800}
1801
1802fn format_source_compact(source: &envx_core::EnvVarSource) -> console::StyledObject<String> {
1803 match source {
1804 envx_core::EnvVarSource::System => style("[SYS]".to_string()).red().bold(),
1805 envx_core::EnvVarSource::User => style("[USR]".to_string()).yellow().bold(),
1806 envx_core::EnvVarSource::Process => style("[PRC]".to_string()).green().bold(),
1807 envx_core::EnvVarSource::Shell => style("[SHL]".to_string()).cyan().bold(),
1808 envx_core::EnvVarSource::Application(app) => style(format!("[{}]", &app[..3.min(app.len())].to_uppercase()))
1809 .magenta()
1810 .bold(),
1811 }
1812}
1813
1814fn truncate_value(value: &str, max_len: usize) -> String {
1815 if value.len() <= max_len {
1816 value.to_string()
1817 } else {
1818 format!("{}...", &value[..max_len - 3])
1819 }
1820}
1821
1822#[allow(clippy::too_many_lines)]
1836pub fn handle_project(args: ProjectArgs) -> Result<()> {
1837 match args.command {
1838 ProjectCommands::Init { name } => {
1839 let manager = ProjectManager::new()?;
1840 manager.init(name)?;
1841 }
1842
1843 ProjectCommands::Apply { force } => {
1844 let mut project = ProjectManager::new()?;
1845 let mut env_manager = EnvVarManager::new();
1846 let mut profile_manager = ProfileManager::new()?;
1847
1848 if let Some(project_dir) = project.find_and_load()? {
1849 println!("📁 Found project at: {}", project_dir.display());
1850
1851 let report = project.validate(&env_manager)?;
1853
1854 if !report.success && !force {
1855 print_validation_report(&report);
1856 return Err(color_eyre::eyre::eyre!(
1857 "Validation failed. Use --force to apply anyway."
1858 ));
1859 }
1860
1861 project.apply(&mut env_manager, &mut profile_manager)?;
1863 println!("✅ Applied project configuration");
1864
1865 if !report.warnings.is_empty() {
1866 println!("\n⚠️ Warnings:");
1867 for warning in &report.warnings {
1868 println!(" - {}: {}", warning.var_name, warning.message);
1869 }
1870 }
1871 } else {
1872 return Err(color_eyre::eyre::eyre!(
1873 "No .envx/config.yaml found in current or parent directories"
1874 ));
1875 }
1876 }
1877
1878 ProjectCommands::Check => {
1879 let mut project = ProjectManager::new()?;
1880 let env_manager = EnvVarManager::new();
1881
1882 if project.find_and_load()?.is_some() {
1883 let report = project.validate(&env_manager)?;
1884 print_validation_report(&report);
1885
1886 if !report.success {
1887 std::process::exit(1);
1888 }
1889 } else {
1890 return Err(color_eyre::eyre::eyre!("No project configuration found"));
1891 }
1892 }
1893
1894 ProjectCommands::Edit => {
1895 let _ = ProjectManager::new()?;
1896 let config_path = std::env::current_dir()?.join(".envx").join("config.yaml");
1897
1898 if !config_path.exists() {
1899 return Err(color_eyre::eyre::eyre!(
1900 "No .envx/config.yaml found. Run 'envx init' first."
1901 ));
1902 }
1903
1904 #[cfg(windows)]
1905 {
1906 std::process::Command::new("notepad").arg(&config_path).spawn()?;
1907 }
1908
1909 #[cfg(unix)]
1910 {
1911 let editor = std::env::var("EDITOR").unwrap_or_else(|_| "nano".to_string());
1912 std::process::Command::new(editor).arg(&config_path).spawn()?;
1913 }
1914
1915 println!("📝 Opening config in editor...");
1916 }
1917
1918 ProjectCommands::Info => {
1919 let mut project = ProjectManager::new()?;
1920
1921 if let Some(project_dir) = project.find_and_load()? {
1922 let config_path = project_dir.join(".envx").join("config.yaml");
1924 let content = std::fs::read_to_string(&config_path)?;
1925
1926 println!("📁 Project Directory: {}", project_dir.display());
1927 println!("\n📄 Configuration:");
1928 println!("{content}");
1929 } else {
1930 return Err(color_eyre::eyre::eyre!("No project configuration found"));
1931 }
1932 }
1933
1934 ProjectCommands::Run { script } => {
1935 let mut project = ProjectManager::new()?;
1936 let mut env_manager = EnvVarManager::new();
1937
1938 if project.find_and_load()?.is_some() {
1939 project.run_script(&script, &mut env_manager)?;
1940 println!("✅ Script '{script}' completed");
1941 } else {
1942 return Err(color_eyre::eyre::eyre!("No project configuration found"));
1943 }
1944 }
1945
1946 ProjectCommands::Require {
1947 name,
1948 description,
1949 pattern,
1950 example,
1951 } => {
1952 let config_path = std::env::current_dir()?.join(".envx").join("config.yaml");
1953
1954 if !config_path.exists() {
1955 return Err(color_eyre::eyre::eyre!(
1956 "No .envx/config.yaml found. Run 'envx init' first."
1957 ));
1958 }
1959
1960 let mut config = ProjectConfig::load(&config_path)?;
1962 config.required.push(RequiredVar {
1963 name: name.clone(),
1964 description,
1965 pattern,
1966 example,
1967 });
1968 config.save(&config_path)?;
1969
1970 println!("✅ Added required variable: {name}");
1971 }
1972 }
1973
1974 Ok(())
1975}
1976
1977fn print_validation_report(report: &ValidationReport) {
1978 if report.success {
1979 println!("✅ All required variables are set!");
1980 return;
1981 }
1982
1983 if !report.missing.is_empty() {
1984 println!("❌ Missing required variables:");
1985 let mut table = Table::new();
1986 table.set_header(vec!["Variable", "Description", "Example"]);
1987
1988 for var in &report.missing {
1989 table.add_row(vec![
1990 var.name.clone(),
1991 var.description.clone().unwrap_or_default(),
1992 var.example.clone().unwrap_or_default(),
1993 ]);
1994 }
1995
1996 println!("{table}");
1997 }
1998
1999 if !report.errors.is_empty() {
2000 println!("\n❌ Validation errors:");
2001 for error in &report.errors {
2002 println!(" - {}: {}", error.var_name, error.message);
2003 }
2004 }
2005}
2006
2007pub fn handle_rename(args: &RenameArgs) -> Result<()> {
2018 let mut manager = EnvVarManager::new();
2019 manager.load_all()?;
2020
2021 if args.dry_run {
2022 let preview = preview_rename(&manager, &args.pattern, &args.replacement)?;
2024
2025 if preview.is_empty() {
2026 println!("No variables match the pattern '{}'", args.pattern);
2027 } else {
2028 println!("Would rename {} variable(s):", preview.len());
2029
2030 let mut table = Table::new();
2031 table.load_preset(UTF8_FULL);
2032 table.set_header(vec!["Current Name", "New Name", "Value"]);
2033
2034 for (old, new, value) in preview {
2035 table.add_row(vec![old, new, value]);
2036 }
2037
2038 println!("{table}");
2039 println!("\nUse without --dry-run to apply changes");
2040 }
2041 } else {
2042 let renamed = manager.rename(&args.pattern, &args.replacement)?;
2043
2044 if renamed.is_empty() {
2045 println!("No variables match the pattern '{}'", args.pattern);
2046 } else {
2047 println!("✅ Renamed {} variable(s):", renamed.len());
2048
2049 let mut table = Table::new();
2050 table.load_preset(UTF8_FULL);
2051 table.set_header(vec!["Old Name", "New Name"]);
2052
2053 for (old, new) in &renamed {
2054 table.add_row(vec![old.clone(), new.clone()]);
2055 }
2056
2057 println!("{table}");
2058
2059 #[cfg(windows)]
2060 println!("\n📝 Note: You may need to restart your terminal for changes to take effect");
2061 }
2062 }
2063
2064 Ok(())
2065}
2066
2067fn preview_rename(manager: &EnvVarManager, pattern: &str, replacement: &str) -> Result<Vec<(String, String, String)>> {
2068 let mut preview = Vec::new();
2069
2070 if pattern.contains('*') {
2071 let (prefix, suffix) = split_wildcard_pattern(pattern)?;
2072 let (new_prefix, new_suffix) = split_wildcard_pattern(replacement)?;
2073
2074 for var in manager.list() {
2075 if var.name.starts_with(&prefix)
2076 && var.name.ends_with(&suffix)
2077 && var.name.len() >= prefix.len() + suffix.len()
2078 {
2079 let middle = &var.name[prefix.len()..var.name.len() - suffix.len()];
2080 let new_name = format!("{new_prefix}{middle}{new_suffix}");
2081 preview.push((var.name.clone(), new_name, var.value.clone()));
2082 }
2083 }
2084 } else if let Some(var) = manager.get(pattern) {
2085 preview.push((var.name.clone(), replacement.to_string(), var.value.clone()));
2086 }
2087
2088 Ok(preview)
2089}
2090
2091pub fn handle_replace(args: &ReplaceArgs) -> Result<()> {
2101 let mut manager = EnvVarManager::new();
2102 manager.load_all()?;
2103
2104 if args.dry_run {
2105 let preview = preview_replace(&manager, &args.pattern)?;
2107
2108 if preview.is_empty() {
2109 println!("No variables match the pattern '{}'", args.pattern);
2110 } else {
2111 println!("Would update {} variable(s):", preview.len());
2112
2113 let mut table = Table::new();
2114 table.load_preset(UTF8_FULL);
2115 table.set_header(vec!["Variable", "Current Value", "New Value"]);
2116
2117 for (name, current) in preview {
2118 table.add_row(vec![name, current, args.value.clone()]);
2119 }
2120
2121 println!("{table}");
2122 println!("\nUse without --dry-run to apply changes");
2123 }
2124 } else {
2125 let replaced = manager.replace(&args.pattern, &args.value)?;
2126
2127 if replaced.is_empty() {
2128 println!("No variables match the pattern '{}'", args.pattern);
2129 } else {
2130 println!("✅ Updated {} variable(s):", replaced.len());
2131
2132 let mut table = Table::new();
2133 table.load_preset(UTF8_FULL);
2134 table.set_header(vec!["Variable", "Old Value", "New Value"]);
2135
2136 for (name, old, new) in &replaced {
2137 let display_old = if old.len() > 50 {
2139 format!("{}...", &old[..47])
2140 } else {
2141 old.clone()
2142 };
2143 let display_new = if new.len() > 50 {
2144 format!("{}...", &new[..47])
2145 } else {
2146 new.clone()
2147 };
2148 table.add_row(vec![name.clone(), display_old, display_new]);
2149 }
2150
2151 println!("{table}");
2152
2153 #[cfg(windows)]
2154 println!("\n📝 Note: You may need to restart your terminal for changes to take effect");
2155 }
2156 }
2157
2158 Ok(())
2159}
2160
2161fn preview_replace(manager: &EnvVarManager, pattern: &str) -> Result<Vec<(String, String)>> {
2162 let mut preview = Vec::new();
2163
2164 if pattern.contains('*') {
2165 let (prefix, suffix) = split_wildcard_pattern(pattern)?;
2166
2167 for var in manager.list() {
2168 if var.name.starts_with(&prefix)
2169 && var.name.ends_with(&suffix)
2170 && var.name.len() >= prefix.len() + suffix.len()
2171 {
2172 preview.push((var.name.clone(), var.value.clone()));
2173 }
2174 }
2175 } else if let Some(var) = manager.get(pattern) {
2176 preview.push((var.name.clone(), var.value.clone()));
2177 }
2178
2179 Ok(preview)
2180}
2181
2182pub fn handle_find_replace(args: &FindReplaceArgs) -> Result<()> {
2193 let mut manager = EnvVarManager::new();
2194 manager.load_all()?;
2195
2196 if args.dry_run {
2197 let preview = preview_find_replace(&manager, &args.search, &args.replacement, args.pattern.as_deref())?;
2199
2200 if preview.is_empty() {
2201 println!("No variables contain '{}'", args.search);
2202 } else {
2203 println!("Would update {} variable(s):", preview.len());
2204
2205 let mut table = Table::new();
2206 table.load_preset(UTF8_FULL);
2207 table.set_header(vec!["Variable", "Current Value", "New Value"]);
2208
2209 for (name, old, new) in preview {
2210 table.add_row(vec![name, old, new]);
2211 }
2212
2213 println!("{table}");
2214 println!("\nUse without --dry-run to apply changes");
2215 }
2216 } else {
2217 let replaced = manager.find_replace(&args.search, &args.replacement, args.pattern.as_deref())?;
2218
2219 if replaced.is_empty() {
2220 println!("No variables contain '{}'", args.search);
2221 } else {
2222 println!("✅ Updated {} variable(s):", replaced.len());
2223
2224 let mut table = Table::new();
2225 table.load_preset(UTF8_FULL);
2226 table.set_header(vec!["Variable", "Old Value", "New Value"]);
2227
2228 for (name, old, new) in &replaced {
2229 let display_old = if old.len() > 50 {
2231 format!("{}...", &old[..47])
2232 } else {
2233 old.clone()
2234 };
2235 let display_new = if new.len() > 50 {
2236 format!("{}...", &new[..47])
2237 } else {
2238 new.clone()
2239 };
2240 table.add_row(vec![name.clone(), display_old, display_new]);
2241 }
2242
2243 println!("{table}");
2244
2245 #[cfg(windows)]
2246 println!("\n📝 Note: You may need to restart your terminal for changes to take effect");
2247 }
2248 }
2249
2250 Ok(())
2251}
2252
2253fn preview_find_replace(
2254 manager: &EnvVarManager,
2255 search: &str,
2256 replacement: &str,
2257 pattern: Option<&str>,
2258) -> Result<Vec<(String, String, String)>> {
2259 let mut preview = Vec::new();
2260
2261 for var in manager.list() {
2262 let matches_pattern = if let Some(pat) = pattern {
2264 if pat.contains('*') {
2265 let (prefix, suffix) = split_wildcard_pattern(pat)?;
2266 var.name.starts_with(&prefix)
2267 && var.name.ends_with(&suffix)
2268 && var.name.len() >= prefix.len() + suffix.len()
2269 } else {
2270 var.name == pat
2271 }
2272 } else {
2273 true
2274 };
2275
2276 if matches_pattern && var.value.contains(search) {
2277 let new_value = var.value.replace(search, replacement);
2278 preview.push((var.name.clone(), var.value.clone(), new_value));
2279 }
2280 }
2281
2282 Ok(preview)
2283}