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 clap::{Parser, Subcommand};
26use color_eyre::Result;
27use color_eyre::eyre::eyre;
28use envx_core::{Analyzer, EnvVarManager, ExportFormat, Exporter, ImportFormat, Importer};
29use std::io::Write;
30use std::path::Path;
31#[derive(Parser)]
32#[command(name = "envx")]
33#[command(about = "System Environment Variable Manager")]
34#[command(version)]
35pub struct Cli {
36 #[command(subcommand)]
37 pub command: Commands,
38}
39
40#[derive(Subcommand)]
41pub enum Commands {
42 List {
44 #[arg(short, long)]
46 source: Option<String>,
47
48 #[arg(short = 'q', long)]
50 query: Option<String>,
51
52 #[arg(short, long, default_value = "table")]
54 format: String,
55
56 #[arg(long, default_value = "name")]
58 sort: String,
59
60 #[arg(long)]
62 names_only: bool,
63
64 #[arg(short, long)]
66 limit: Option<usize>,
67
68 #[arg(long)]
70 stats: bool,
71 },
72
73 Get {
75 pattern: String,
84
85 #[arg(short, long, default_value = "simple")]
87 format: String,
88 },
89
90 Set {
92 name: String,
94
95 value: String,
97
98 #[arg(short, long)]
100 temporary: bool,
101 },
102
103 Delete {
105 pattern: String,
107
108 #[arg(short, long)]
110 force: bool,
111 },
112
113 Analyze {
115 #[arg(short, long, default_value = "all")]
117 analysis_type: String,
118 },
119
120 #[command(visible_alias = "ui")]
122 Tui,
123
124 Path {
126 #[command(subcommand)]
127 action: Option<PathAction>,
128
129 #[arg(short, long)]
131 check: bool,
132
133 #[arg(short = 'v', long, default_value = "PATH")]
135 var: String,
136
137 #[arg(short = 'p', long)]
139 permanent: bool,
140 },
141
142 Export {
144 file: String,
146
147 #[arg(short = 'v', long)]
149 vars: Vec<String>,
150
151 #[arg(short, long)]
153 format: Option<String>,
154
155 #[arg(short, long)]
157 source: Option<String>,
158
159 #[arg(short, long)]
161 metadata: bool,
162
163 #[arg(long)]
165 force: bool,
166 },
167
168 Import {
170 file: String,
172
173 #[arg(short = 'v', long)]
175 vars: Vec<String>,
176
177 #[arg(short, long)]
179 format: Option<String>,
180
181 #[arg(short, long)]
183 permanent: bool,
184
185 #[arg(long)]
187 prefix: Option<String>,
188
189 #[arg(long)]
191 overwrite: bool,
192
193 #[arg(short = 'n', long)]
195 dry_run: bool,
196 },
197
198 Snapshot(SnapshotArgs),
200
201 Profile(ProfileArgs),
203
204 Project(ProjectArgs),
206
207 Rename(RenameArgs),
209
210 Replace(ReplaceArgs),
212
213 FindReplace(FindReplaceArgs),
215
216 Watch(WatchArgs),
218
219 Monitor(MonitorArgs),
221
222 Docs(DocsArgs),
224
225 Deps(DepsArgs),
227
228 Cleanup(CleanupArgs),
230}
231
232#[derive(Subcommand)]
233pub enum PathAction {
234 Add {
236 directory: String,
238
239 #[arg(short, long)]
241 first: bool,
242
243 #[arg(short, long)]
245 create: bool,
246 },
247
248 Remove {
250 directory: String,
252
253 #[arg(short, long)]
255 all: bool,
256 },
257
258 Clean {
260 #[arg(short, long)]
262 dedupe: bool,
263
264 #[arg(short = 'n', long)]
266 dry_run: bool,
267 },
268
269 Dedupe {
271 #[arg(short, long)]
273 keep_first: bool,
274
275 #[arg(short = 'n', long)]
277 dry_run: bool,
278 },
279
280 Check {
282 #[arg(short, long)]
284 verbose: bool,
285 },
286
287 List {
289 #[arg(short, long)]
291 numbered: bool,
292
293 #[arg(short, long)]
295 check: bool,
296 },
297
298 Move {
300 from: String,
302
303 to: String,
305 },
306}
307
308pub fn execute(cli: Cli) -> Result<()> {
319 match cli.command {
320 Commands::List {
321 source,
322 query,
323 format,
324 sort,
325 names_only,
326 limit,
327 stats,
328 } => {
329 handle_list_command(
330 source.as_deref(),
331 query.as_deref(),
332 &format,
333 &sort,
334 names_only,
335 limit,
336 stats,
337 )?;
338 }
339
340 Commands::Get { pattern, format } => {
341 handle_get_command(&pattern, &format)?;
342 }
343
344 Commands::Set { name, value, temporary } => {
345 handle_set_command(&name, &value, temporary)?;
346 }
347
348 Commands::Delete { pattern, force } => {
349 handle_delete_command(&pattern, force)?;
350 }
351
352 Commands::Analyze { analysis_type } => {
353 handle_analyze_command(&analysis_type)?;
354 }
355
356 Commands::Tui => {
357 envx_tui::run()?;
359 }
360
361 Commands::Path {
362 action,
363 check,
364 var,
365 permanent,
366 } => {
367 handle_path_command(action, check, &var, permanent)?;
368 }
369
370 Commands::Export {
371 file,
372 vars,
373 format,
374 source,
375 metadata,
376 force,
377 } => {
378 handle_export(&file, &vars, format, source, metadata, force)?;
379 }
380
381 Commands::Import {
382 file,
383 vars,
384 format,
385 permanent,
386 prefix,
387 overwrite,
388 dry_run,
389 } => {
390 handle_import(&file, &vars, format, permanent, prefix.as_ref(), overwrite, dry_run)?;
391 }
392
393 Commands::Snapshot(args) => {
394 handle_snapshot(args)?;
395 }
396 Commands::Profile(args) => {
397 handle_profile(args)?;
398 }
399
400 Commands::Project(args) => {
401 handle_project(args)?;
402 }
403
404 Commands::Rename(args) => {
405 handle_rename(&args)?;
406 }
407
408 Commands::Replace(args) => {
409 handle_replace(&args)?;
410 }
411
412 Commands::FindReplace(args) => {
413 handle_find_replace(&args)?;
414 }
415
416 Commands::Watch(args) => {
417 handle_watch(&args)?;
418 }
419
420 Commands::Monitor(args) => {
421 handle_monitor(args)?;
422 }
423
424 Commands::Docs(args) => {
425 handle_docs(args)?;
426 }
427
428 Commands::Deps(args) => {
429 handle_deps(&args)?;
430 }
431
432 Commands::Cleanup(args) => {
433 handle_cleanup(&args)?;
434 }
435 }
436
437 Ok(())
438}
439
440fn handle_get_command(pattern: &str, format: &str) -> Result<()> {
441 let mut manager = EnvVarManager::new();
442 manager.load_all()?;
443
444 let vars = manager.get_pattern(pattern);
445
446 if vars.is_empty() {
447 eprintln!("No variables found matching pattern: {pattern}");
448 return Ok(());
449 }
450
451 match format {
452 "json" => {
453 println!("{}", serde_json::to_string_pretty(&vars)?);
454 }
455 "detailed" => {
456 for var in vars {
457 println!("Name: {}", var.name);
458 println!("Value: {}", var.value);
459 println!("Source: {:?}", var.source);
460 println!("Modified: {}", var.modified.format("%Y-%m-%d %H:%M:%S"));
461 if let Some(orig) = &var.original_value {
462 println!("Original: {orig}");
463 }
464 println!("---");
465 }
466 }
467 _ => {
468 for var in vars {
469 println!("{} = {}", var.name, var.value);
470 }
471 }
472 }
473 Ok(())
474}
475
476fn handle_set_command(name: &str, value: &str, temporary: bool) -> Result<()> {
477 let mut manager = EnvVarManager::new();
478 manager.load_all()?;
479
480 let permanent = !temporary;
481
482 manager.set(name, value, permanent)?;
483 if permanent {
484 println!("✅ Set {name} = \"{value}\"");
485 #[cfg(windows)]
486 println!("📝 Note: You may need to restart your terminal for changes to take effect");
487 } else {
488 println!("⚡ Set {name} = \"{value}\" (temporary - current session only)");
489 }
490 Ok(())
491}
492
493fn handle_delete_command(pattern: &str, force: bool) -> Result<()> {
494 let mut manager = EnvVarManager::new();
495 manager.load_all()?;
496
497 let vars_to_delete: Vec<String> = manager
499 .get_pattern(pattern)
500 .into_iter()
501 .map(|v| v.name.clone())
502 .collect();
503
504 if vars_to_delete.is_empty() {
505 eprintln!("No variables found matching pattern: {pattern}");
506 return Ok(());
507 }
508
509 if !force && vars_to_delete.len() > 1 {
510 println!("About to delete {} variables:", vars_to_delete.len());
511 for name in &vars_to_delete {
512 println!(" - {name}");
513 }
514 print!("Continue? [y/N]: ");
515 std::io::stdout().flush()?;
516
517 let mut input = String::new();
518 std::io::stdin().read_line(&mut input)?;
519
520 if !input.trim().eq_ignore_ascii_case("y") {
521 println!("Cancelled.");
522 return Ok(());
523 }
524 }
525
526 for name in vars_to_delete {
528 manager.delete(&name)?;
529 println!("Deleted: {name}");
530 }
531 Ok(())
532}
533
534fn handle_analyze_command(analysis_type: &str) -> Result<()> {
535 let mut manager = EnvVarManager::new();
536 manager.load_all()?;
537 let vars = manager.list().into_iter().cloned().collect();
538 let analyzer = Analyzer::new(vars);
539
540 match analysis_type {
541 "duplicates" | "all" => {
542 let duplicates = analyzer.find_duplicates();
543 if !duplicates.is_empty() {
544 println!("Duplicate variables found:");
545 for (name, vars) in duplicates {
546 println!(" {}: {} instances", name, vars.len());
547 }
548 }
549 }
550 "invalid" => {
551 let validation = analyzer.validate_all();
552 for (name, result) in validation {
553 if !result.valid {
554 println!("Invalid variable: {name}");
555 for error in result.errors {
556 println!(" Error: {error}");
557 }
558 }
559 }
560 }
561 _ => {}
562 }
563 Ok(())
564}
565
566fn handle_export(
567 file: &str,
568 vars: &[String],
569 format: Option<String>,
570 source: Option<String>,
571 metadata: bool,
572 force: bool,
573) -> Result<()> {
574 if Path::new(&file).exists() && !force {
576 print!("File '{file}' already exists. Overwrite? [y/N]: ");
577 std::io::stdout().flush()?;
578
579 let mut input = String::new();
580 std::io::stdin().read_line(&mut input)?;
581
582 if !input.trim().eq_ignore_ascii_case("y") {
583 println!("Export cancelled.");
584 return Ok(());
585 }
586 }
587
588 let mut manager = EnvVarManager::new();
590 manager.load_all()?;
591
592 let mut vars_to_export = if vars.is_empty() {
594 manager.list().into_iter().cloned().collect()
595 } else {
596 let mut selected = Vec::new();
597 for pattern in vars {
598 let matched = manager.get_pattern(pattern);
599 selected.extend(matched.into_iter().cloned());
600 }
601 selected
602 };
603
604 if let Some(src) = source {
606 let source_filter = match src.as_str() {
607 "system" => envx_core::EnvVarSource::System,
608 "user" => envx_core::EnvVarSource::User,
609 "process" => envx_core::EnvVarSource::Process,
610 "shell" => envx_core::EnvVarSource::Shell,
611 _ => return Err(eyre!("Invalid source: {}", src)),
612 };
613
614 vars_to_export.retain(|v| v.source == source_filter);
615 }
616
617 if vars_to_export.is_empty() {
618 println!("No variables to export.");
619 return Ok(());
620 }
621
622 let export_format = if let Some(fmt) = format {
624 match fmt.as_str() {
625 "env" => ExportFormat::DotEnv,
626 "json" => ExportFormat::Json,
627 "yaml" | "yml" => ExportFormat::Yaml,
628 "txt" | "text" => ExportFormat::Text,
629 "ps1" | "powershell" => ExportFormat::PowerShell,
630 "sh" | "bash" => ExportFormat::Shell,
631 _ => return Err(eyre!("Unsupported format: {}", fmt)),
632 }
633 } else {
634 ExportFormat::from_extension(file)?
636 };
637
638 let exporter = Exporter::new(vars_to_export, metadata);
640 exporter.export_to_file(file, export_format)?;
641
642 println!("Exported {} variables to '{}'", exporter.count(), file);
643
644 Ok(())
645}
646
647fn handle_import(
648 file: &str,
649 vars: &[String],
650 format: Option<String>,
651 permanent: bool,
652 prefix: Option<&String>,
653 overwrite: bool,
654 dry_run: bool,
655) -> Result<()> {
656 if !Path::new(&file).exists() {
658 return Err(eyre!("File not found: {}", file));
659 }
660
661 let import_format = if let Some(fmt) = format {
663 match fmt.as_str() {
664 "env" => ImportFormat::DotEnv,
665 "json" => ImportFormat::Json,
666 "yaml" | "yml" => ImportFormat::Yaml,
667 "txt" | "text" => ImportFormat::Text,
668 _ => return Err(eyre!("Unsupported format: {}", fmt)),
669 }
670 } else {
671 ImportFormat::from_extension(file)?
673 };
674
675 let mut importer = Importer::new();
677 importer.import_from_file(file, import_format)?;
678
679 if !vars.is_empty() {
681 importer.filter_by_patterns(vars);
682 }
683
684 if let Some(pfx) = &prefix {
686 importer.add_prefix(pfx);
687 }
688
689 let import_vars = importer.get_variables();
691
692 if import_vars.is_empty() {
693 println!("No variables to import.");
694 return Ok(());
695 }
696
697 let mut manager = EnvVarManager::new();
699 manager.load_all()?;
700
701 let mut conflicts = Vec::new();
702 for (name, _) in &import_vars {
703 if manager.get(name).is_some() {
704 conflicts.push(name.clone());
705 }
706 }
707
708 if !conflicts.is_empty() && !overwrite && !dry_run {
709 println!("The following variables already exist:");
710 for name in &conflicts {
711 println!(" - {name}");
712 }
713
714 print!("Overwrite existing variables? [y/N]: ");
715 std::io::stdout().flush()?;
716
717 let mut input = String::new();
718 std::io::stdin().read_line(&mut input)?;
719
720 if !input.trim().eq_ignore_ascii_case("y") {
721 println!("Import cancelled.");
722 return Ok(());
723 }
724 }
725
726 if dry_run {
728 println!("Would import {} variables:", import_vars.len());
729 for (name, value) in &import_vars {
730 let status = if conflicts.contains(name) {
731 " [OVERWRITE]"
732 } else {
733 " [NEW]"
734 };
735 println!(
736 " {} = {}{}",
737 name,
738 if value.len() > 50 {
739 format!("{}...", &value[..50])
740 } else {
741 value.clone()
742 },
743 status
744 );
745 }
746 println!("\n(Dry run - no changes made)");
747 } else {
748 let mut imported = 0;
750 let mut failed = 0;
751
752 for (name, value) in import_vars {
753 match manager.set(&name, &value, permanent) {
754 Ok(()) => imported += 1,
755 Err(e) => {
756 eprintln!("Failed to import {name}: {e}");
757 failed += 1;
758 }
759 }
760 }
761
762 println!("Imported {imported} variables");
763 if failed > 0 {
764 println!("Failed to import {failed} variables");
765 }
766 }
767
768 Ok(())
769}