1use crate::CleanupArgs;
2use crate::DepsArgs;
3use crate::DocsArgs;
4use crate::MonitorArgs;
5use crate::ProfileArgs;
6use crate::ProjectArgs;
7use crate::RenameArgs;
8use crate::SnapshotArgs;
9use crate::WatchArgs;
10use crate::handle_cleanup;
11use crate::handle_deps;
12use crate::handle_docs;
13use crate::handle_find_replace;
14use crate::handle_list_command;
15use crate::handle_path_command;
16use crate::handle_profile;
17use crate::handle_project;
18use crate::handle_rename;
19use crate::handle_replace;
20use crate::handle_snapshot;
21use crate::handle_watch;
22use crate::monitor::handle_monitor;
23use crate::replace::FindReplaceArgs;
24use crate::replace::ReplaceArgs;
25use crate::wizard::list_templates as list_templates_func;
26use crate::wizard::run_wizard;
27use clap::{Parser, Subcommand};
28use color_eyre::Result;
29use color_eyre::eyre::eyre;
30use envx_core::{Analyzer, EnvVarManager, ExportFormat, Exporter, ImportFormat, Importer};
31use std::io::Write;
32use std::path::Path;
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 Init {
46 #[arg(short, long)]
48 template: Option<String>,
49
50 #[arg(short, long, default_value = "true")]
52 wizard: bool,
53
54 #[arg(long)]
56 list_templates: bool,
57 },
58 List {
60 #[arg(short, long)]
62 source: Option<String>,
63
64 #[arg(short = 'q', long)]
66 query: Option<String>,
67
68 #[arg(short, long, default_value = "table")]
70 format: String,
71
72 #[arg(long, default_value = "name")]
74 sort: String,
75
76 #[arg(long)]
78 names_only: bool,
79
80 #[arg(short, long)]
82 limit: Option<usize>,
83
84 #[arg(long)]
86 stats: bool,
87 },
88
89 Get {
91 pattern: String,
100
101 #[arg(short, long, default_value = "simple")]
103 format: String,
104 },
105
106 Set {
108 name: String,
110
111 value: String,
113
114 #[arg(short, long)]
116 temporary: bool,
117 },
118
119 Delete {
121 pattern: String,
123
124 #[arg(short, long)]
126 force: bool,
127 },
128
129 Analyze {
131 #[arg(short, long, default_value = "all")]
133 analysis_type: String,
134 },
135
136 #[command(visible_alias = "ui")]
138 Tui,
139
140 Path {
142 #[command(subcommand)]
143 action: Option<PathAction>,
144
145 #[arg(short, long)]
147 check: bool,
148
149 #[arg(short = 'v', long, default_value = "PATH")]
151 var: String,
152
153 #[arg(short = 'p', long)]
155 permanent: bool,
156 },
157
158 Export {
160 file: String,
162
163 #[arg(short = 'v', long)]
165 vars: Vec<String>,
166
167 #[arg(short, long)]
169 format: Option<String>,
170
171 #[arg(short, long)]
173 source: Option<String>,
174
175 #[arg(short, long)]
177 metadata: bool,
178
179 #[arg(long)]
181 force: bool,
182 },
183
184 Import {
186 file: String,
188
189 #[arg(short = 'v', long)]
191 vars: Vec<String>,
192
193 #[arg(short, long)]
195 format: Option<String>,
196
197 #[arg(short, long)]
199 permanent: bool,
200
201 #[arg(long)]
203 prefix: Option<String>,
204
205 #[arg(long)]
207 overwrite: bool,
208
209 #[arg(short = 'n', long)]
211 dry_run: bool,
212 },
213
214 Snapshot(SnapshotArgs),
216
217 Profile(ProfileArgs),
219
220 Project(ProjectArgs),
222
223 Rename(RenameArgs),
225
226 Replace(ReplaceArgs),
228
229 FindReplace(FindReplaceArgs),
231
232 Watch(WatchArgs),
234
235 Monitor(MonitorArgs),
237
238 Docs(DocsArgs),
240
241 Deps(DepsArgs),
243
244 Cleanup(CleanupArgs),
246}
247
248#[derive(Subcommand)]
249pub enum PathAction {
250 Add {
252 directory: String,
254
255 #[arg(short, long)]
257 first: bool,
258
259 #[arg(short, long)]
261 create: bool,
262 },
263
264 Remove {
266 directory: String,
268
269 #[arg(short, long)]
271 all: bool,
272 },
273
274 Clean {
276 #[arg(short, long)]
278 dedupe: bool,
279
280 #[arg(short = 'n', long)]
282 dry_run: bool,
283 },
284
285 Dedupe {
287 #[arg(short, long)]
289 keep_first: bool,
290
291 #[arg(short = 'n', long)]
293 dry_run: bool,
294 },
295
296 Check {
298 #[arg(short, long)]
300 verbose: bool,
301 },
302
303 List {
305 #[arg(short, long)]
307 numbered: bool,
308
309 #[arg(short, long)]
311 check: bool,
312 },
313
314 Move {
316 from: String,
318
319 to: String,
321 },
322}
323
324#[allow(clippy::too_many_lines)]
335pub fn execute(cli: Cli) -> Result<()> {
336 match cli.command {
337 Commands::List {
338 source,
339 query,
340 format,
341 sort,
342 names_only,
343 limit,
344 stats,
345 } => {
346 handle_list_command(
347 source.as_deref(),
348 query.as_deref(),
349 &format,
350 &sort,
351 names_only,
352 limit,
353 stats,
354 )?;
355 }
356
357 Commands::Get { pattern, format } => {
358 handle_get_command(&pattern, &format)?;
359 }
360
361 Commands::Set { name, value, temporary } => {
362 handle_set_command(&name, &value, temporary)?;
363 }
364
365 Commands::Delete { pattern, force } => {
366 handle_delete_command(&pattern, force)?;
367 }
368
369 Commands::Analyze { analysis_type } => {
370 handle_analyze_command(&analysis_type)?;
371 }
372
373 Commands::Tui => {
374 envx_tui::run()?;
376 }
377
378 Commands::Path {
379 action,
380 check,
381 var,
382 permanent,
383 } => {
384 handle_path_command(action, check, &var, permanent)?;
385 }
386
387 Commands::Export {
388 file,
389 vars,
390 format,
391 source,
392 metadata,
393 force,
394 } => {
395 handle_export(&file, &vars, format, source, metadata, force)?;
396 }
397
398 Commands::Import {
399 file,
400 vars,
401 format,
402 permanent,
403 prefix,
404 overwrite,
405 dry_run,
406 } => {
407 handle_import(&file, &vars, format, permanent, prefix.as_ref(), overwrite, dry_run)?;
408 }
409
410 Commands::Snapshot(args) => {
411 handle_snapshot(args)?;
412 }
413 Commands::Profile(args) => {
414 handle_profile(args)?;
415 }
416
417 Commands::Project(args) => {
418 handle_project(args)?;
419 }
420
421 Commands::Rename(args) => {
422 handle_rename(&args)?;
423 }
424
425 Commands::Replace(args) => {
426 handle_replace(&args)?;
427 }
428
429 Commands::FindReplace(args) => {
430 handle_find_replace(&args)?;
431 }
432
433 Commands::Watch(args) => {
434 handle_watch(&args)?;
435 }
436
437 Commands::Monitor(args) => {
438 handle_monitor(args)?;
439 }
440
441 Commands::Docs(args) => {
442 handle_docs(args)?;
443 }
444
445 Commands::Deps(args) => {
446 handle_deps(&args)?;
447 }
448
449 Commands::Cleanup(args) => {
450 handle_cleanup(&args)?;
451 }
452
453 Commands::Init {
454 template,
455 wizard,
456 list_templates,
457 } => {
458 if list_templates {
459 list_templates_func()?;
460 } else if wizard && template.is_none() {
461 match run_wizard(None) {
462 Ok(()) => {}
463 Err(e) => {
464 eprintln!("Error running wizard: {e}");
465 std::process::exit(1);
466 }
467 }
468 } else if let Some(tmpl) = template {
469 match run_wizard(Some(tmpl)) {
470 Ok(()) => {}
471 Err(e) => {
472 eprintln!("Error running wizard with template: {e}");
473 std::process::exit(1);
474 }
475 }
476 } else {
477 match run_wizard(None) {
478 Ok(()) => {}
479 Err(e) => {
480 eprintln!("Error running wizard: {e}");
481 std::process::exit(1);
482 }
483 }
484 }
485 }
486 }
487
488 Ok(())
489}
490
491fn handle_get_command(pattern: &str, format: &str) -> Result<()> {
492 let mut manager = EnvVarManager::new();
493 manager.load_all()?;
494
495 let vars = manager.get_pattern(pattern);
496
497 if vars.is_empty() {
498 eprintln!("No variables found matching pattern: {pattern}");
499 return Ok(());
500 }
501
502 match format {
503 "json" => {
504 println!("{}", serde_json::to_string_pretty(&vars)?);
505 }
506 "detailed" => {
507 for var in vars {
508 println!("Name: {}", var.name);
509 println!("Value: {}", var.value);
510 println!("Source: {:?}", var.source);
511 println!("Modified: {}", var.modified.format("%Y-%m-%d %H:%M:%S"));
512 if let Some(orig) = &var.original_value {
513 println!("Original: {orig}");
514 }
515 println!("---");
516 }
517 }
518 _ => {
519 for var in vars {
520 println!("{} = {}", var.name, var.value);
521 }
522 }
523 }
524 Ok(())
525}
526
527fn handle_set_command(name: &str, value: &str, temporary: bool) -> Result<()> {
528 let mut manager = EnvVarManager::new();
529 manager.load_all()?;
530
531 let permanent = !temporary;
532
533 manager.set(name, value, permanent)?;
534 if permanent {
535 println!("✅ Set {name} = \"{value}\"");
536 #[cfg(windows)]
537 println!("📝 Note: You may need to restart your terminal for changes to take effect");
538 } else {
539 println!("⚡ Set {name} = \"{value}\" (temporary - current session only)");
540 }
541 Ok(())
542}
543
544fn handle_delete_command(pattern: &str, force: bool) -> Result<()> {
545 let mut manager = EnvVarManager::new();
546 manager.load_all()?;
547
548 let vars_to_delete: Vec<String> = manager
550 .get_pattern(pattern)
551 .into_iter()
552 .map(|v| v.name.clone())
553 .collect();
554
555 if vars_to_delete.is_empty() {
556 eprintln!("No variables found matching pattern: {pattern}");
557 return Ok(());
558 }
559
560 if !force && vars_to_delete.len() > 1 {
561 println!("About to delete {} variables:", vars_to_delete.len());
562 for name in &vars_to_delete {
563 println!(" - {name}");
564 }
565 print!("Continue? [y/N]: ");
566 std::io::stdout().flush()?;
567
568 let mut input = String::new();
569 std::io::stdin().read_line(&mut input)?;
570
571 if !input.trim().eq_ignore_ascii_case("y") {
572 println!("Cancelled.");
573 return Ok(());
574 }
575 }
576
577 for name in vars_to_delete {
579 manager.delete(&name)?;
580 println!("Deleted: {name}");
581 }
582 Ok(())
583}
584
585fn handle_analyze_command(analysis_type: &str) -> Result<()> {
586 let mut manager = EnvVarManager::new();
587 manager.load_all()?;
588 let vars = manager.list().into_iter().cloned().collect();
589 let analyzer = Analyzer::new(vars);
590
591 match analysis_type {
592 "duplicates" | "all" => {
593 let duplicates = analyzer.find_duplicates();
594 if !duplicates.is_empty() {
595 println!("Duplicate variables found:");
596 for (name, vars) in duplicates {
597 println!(" {}: {} instances", name, vars.len());
598 }
599 }
600 }
601 "invalid" => {
602 let validation = analyzer.validate_all();
603 for (name, result) in validation {
604 if !result.valid {
605 println!("Invalid variable: {name}");
606 for error in result.errors {
607 println!(" Error: {error}");
608 }
609 }
610 }
611 }
612 _ => {}
613 }
614 Ok(())
615}
616
617fn handle_export(
618 file: &str,
619 vars: &[String],
620 format: Option<String>,
621 source: Option<String>,
622 metadata: bool,
623 force: bool,
624) -> Result<()> {
625 if Path::new(&file).exists() && !force {
627 print!("File '{file}' already exists. Overwrite? [y/N]: ");
628 std::io::stdout().flush()?;
629
630 let mut input = String::new();
631 std::io::stdin().read_line(&mut input)?;
632
633 if !input.trim().eq_ignore_ascii_case("y") {
634 println!("Export cancelled.");
635 return Ok(());
636 }
637 }
638
639 let mut manager = EnvVarManager::new();
641 manager.load_all()?;
642
643 let mut vars_to_export = if vars.is_empty() {
645 manager.list().into_iter().cloned().collect()
646 } else {
647 let mut selected = Vec::new();
648 for pattern in vars {
649 let matched = manager.get_pattern(pattern);
650 selected.extend(matched.into_iter().cloned());
651 }
652 selected
653 };
654
655 if let Some(src) = source {
657 let source_filter = match src.as_str() {
658 "system" => envx_core::EnvVarSource::System,
659 "user" => envx_core::EnvVarSource::User,
660 "process" => envx_core::EnvVarSource::Process,
661 "shell" => envx_core::EnvVarSource::Shell,
662 _ => return Err(eyre!("Invalid source: {}", src)),
663 };
664
665 vars_to_export.retain(|v| v.source == source_filter);
666 }
667
668 if vars_to_export.is_empty() {
669 println!("No variables to export.");
670 return Ok(());
671 }
672
673 let export_format = if let Some(fmt) = format {
675 match fmt.as_str() {
676 "env" => ExportFormat::DotEnv,
677 "json" => ExportFormat::Json,
678 "yaml" | "yml" => ExportFormat::Yaml,
679 "txt" | "text" => ExportFormat::Text,
680 "ps1" | "powershell" => ExportFormat::PowerShell,
681 "sh" | "bash" => ExportFormat::Shell,
682 _ => return Err(eyre!("Unsupported format: {}", fmt)),
683 }
684 } else {
685 ExportFormat::from_extension(file)?
687 };
688
689 let exporter = Exporter::new(vars_to_export, metadata);
691 exporter.export_to_file(file, export_format)?;
692
693 println!("Exported {} variables to '{}'", exporter.count(), file);
694
695 Ok(())
696}
697
698fn handle_import(
699 file: &str,
700 vars: &[String],
701 format: Option<String>,
702 permanent: bool,
703 prefix: Option<&String>,
704 overwrite: bool,
705 dry_run: bool,
706) -> Result<()> {
707 if !Path::new(&file).exists() {
709 return Err(eyre!("File not found: {}", file));
710 }
711
712 let import_format = if let Some(fmt) = format {
714 match fmt.as_str() {
715 "env" => ImportFormat::DotEnv,
716 "json" => ImportFormat::Json,
717 "yaml" | "yml" => ImportFormat::Yaml,
718 "txt" | "text" => ImportFormat::Text,
719 _ => return Err(eyre!("Unsupported format: {}", fmt)),
720 }
721 } else {
722 ImportFormat::from_extension(file)?
724 };
725
726 let mut importer = Importer::new();
728 importer.import_from_file(file, import_format)?;
729
730 if !vars.is_empty() {
732 importer.filter_by_patterns(vars);
733 }
734
735 if let Some(pfx) = &prefix {
737 importer.add_prefix(pfx);
738 }
739
740 let import_vars = importer.get_variables();
742
743 if import_vars.is_empty() {
744 println!("No variables to import.");
745 return Ok(());
746 }
747
748 let mut manager = EnvVarManager::new();
750 manager.load_all()?;
751
752 let mut conflicts = Vec::new();
753 for (name, _) in &import_vars {
754 if manager.get(name).is_some() {
755 conflicts.push(name.clone());
756 }
757 }
758
759 if !conflicts.is_empty() && !overwrite && !dry_run {
760 println!("The following variables already exist:");
761 for name in &conflicts {
762 println!(" - {name}");
763 }
764
765 print!("Overwrite existing variables? [y/N]: ");
766 std::io::stdout().flush()?;
767
768 let mut input = String::new();
769 std::io::stdin().read_line(&mut input)?;
770
771 if !input.trim().eq_ignore_ascii_case("y") {
772 println!("Import cancelled.");
773 return Ok(());
774 }
775 }
776
777 if dry_run {
779 println!("Would import {} variables:", import_vars.len());
780 for (name, value) in &import_vars {
781 let status = if conflicts.contains(name) {
782 " [OVERWRITE]"
783 } else {
784 " [NEW]"
785 };
786 println!(
787 " {} = {}{}",
788 name,
789 if value.len() > 50 {
790 format!("{}...", &value[..50])
791 } else {
792 value.clone()
793 },
794 status
795 );
796 }
797 println!("\n(Dry run - no changes made)");
798 } else {
799 let mut imported = 0;
801 let mut failed = 0;
802
803 for (name, value) in import_vars {
804 match manager.set(&name, &value, permanent) {
805 Ok(()) => imported += 1,
806 Err(e) => {
807 eprintln!("Failed to import {name}: {e}");
808 failed += 1;
809 }
810 }
811 }
812
813 println!("Imported {imported} variables");
814 if failed > 0 {
815 println!("Failed to import {failed} variables");
816 }
817 }
818
819 Ok(())
820}