1use crate::MonitorArgs;
2use crate::ProfileArgs;
3use crate::ProjectArgs;
4use crate::RenameArgs;
5use crate::SnapshotArgs;
6use crate::WatchArgs;
7use crate::handle_find_replace;
8use crate::handle_list_command;
9use crate::handle_path_command;
10use crate::handle_profile;
11use crate::handle_project;
12use crate::handle_rename;
13use crate::handle_replace;
14use crate::handle_snapshot;
15use crate::handle_watch;
16use crate::monitor::handle_monitor;
17use crate::replace::FindReplaceArgs;
18use crate::replace::ReplaceArgs;
19use clap::{Parser, Subcommand};
20use color_eyre::Result;
21use color_eyre::eyre::eyre;
22use envx_core::{Analyzer, EnvVarManager, ExportFormat, Exporter, ImportFormat, Importer};
23use std::io::Write;
24use std::path::Path;
25#[derive(Parser)]
26#[command(name = "envx")]
27#[command(about = "System Environment Variable Manager")]
28#[command(version)]
29pub struct Cli {
30 #[command(subcommand)]
31 pub command: Commands,
32}
33
34#[derive(Subcommand)]
35pub enum Commands {
36 List {
38 #[arg(short, long)]
40 source: Option<String>,
41
42 #[arg(short = 'q', long)]
44 query: Option<String>,
45
46 #[arg(short, long, default_value = "table")]
48 format: String,
49
50 #[arg(long, default_value = "name")]
52 sort: String,
53
54 #[arg(long)]
56 names_only: bool,
57
58 #[arg(short, long)]
60 limit: Option<usize>,
61
62 #[arg(long)]
64 stats: bool,
65 },
66
67 Get {
69 pattern: String,
78
79 #[arg(short, long, default_value = "simple")]
81 format: String,
82 },
83
84 Set {
86 name: String,
88
89 value: String,
91
92 #[arg(short, long)]
94 temporary: bool,
95 },
96
97 Delete {
99 pattern: String,
101
102 #[arg(short, long)]
104 force: bool,
105 },
106
107 Analyze {
109 #[arg(short, long, default_value = "all")]
111 analysis_type: String,
112 },
113
114 #[command(visible_alias = "ui")]
116 Tui,
117
118 Path {
120 #[command(subcommand)]
121 action: Option<PathAction>,
122
123 #[arg(short, long)]
125 check: bool,
126
127 #[arg(short = 'v', long, default_value = "PATH")]
129 var: String,
130
131 #[arg(short = 'p', long)]
133 permanent: bool,
134 },
135
136 Export {
138 file: String,
140
141 #[arg(short = 'v', long)]
143 vars: Vec<String>,
144
145 #[arg(short, long)]
147 format: Option<String>,
148
149 #[arg(short, long)]
151 source: Option<String>,
152
153 #[arg(short, long)]
155 metadata: bool,
156
157 #[arg(long)]
159 force: bool,
160 },
161
162 Import {
164 file: String,
166
167 #[arg(short = 'v', long)]
169 vars: Vec<String>,
170
171 #[arg(short, long)]
173 format: Option<String>,
174
175 #[arg(short, long)]
177 permanent: bool,
178
179 #[arg(long)]
181 prefix: Option<String>,
182
183 #[arg(long)]
185 overwrite: bool,
186
187 #[arg(short = 'n', long)]
189 dry_run: bool,
190 },
191
192 Snapshot(SnapshotArgs),
194
195 Profile(ProfileArgs),
197
198 Project(ProjectArgs),
200
201 Rename(RenameArgs),
203
204 Replace(ReplaceArgs),
206
207 FindReplace(FindReplaceArgs),
209
210 Watch(WatchArgs),
212
213 Monitor(MonitorArgs),
215}
216
217#[derive(Subcommand)]
218pub enum PathAction {
219 Add {
221 directory: String,
223
224 #[arg(short, long)]
226 first: bool,
227
228 #[arg(short, long)]
230 create: bool,
231 },
232
233 Remove {
235 directory: String,
237
238 #[arg(short, long)]
240 all: bool,
241 },
242
243 Clean {
245 #[arg(short, long)]
247 dedupe: bool,
248
249 #[arg(short = 'n', long)]
251 dry_run: bool,
252 },
253
254 Dedupe {
256 #[arg(short, long)]
258 keep_first: bool,
259
260 #[arg(short = 'n', long)]
262 dry_run: bool,
263 },
264
265 Check {
267 #[arg(short, long)]
269 verbose: bool,
270 },
271
272 List {
274 #[arg(short, long)]
276 numbered: bool,
277
278 #[arg(short, long)]
280 check: bool,
281 },
282
283 Move {
285 from: String,
287
288 to: String,
290 },
291}
292
293pub fn execute(cli: Cli) -> Result<()> {
304 match cli.command {
305 Commands::List {
306 source,
307 query,
308 format,
309 sort,
310 names_only,
311 limit,
312 stats,
313 } => {
314 handle_list_command(
315 source.as_deref(),
316 query.as_deref(),
317 &format,
318 &sort,
319 names_only,
320 limit,
321 stats,
322 )?;
323 }
324
325 Commands::Get { pattern, format } => {
326 handle_get_command(&pattern, &format)?;
327 }
328
329 Commands::Set { name, value, temporary } => {
330 handle_set_command(&name, &value, temporary)?;
331 }
332
333 Commands::Delete { pattern, force } => {
334 handle_delete_command(&pattern, force)?;
335 }
336
337 Commands::Analyze { analysis_type } => {
338 handle_analyze_command(&analysis_type)?;
339 }
340
341 Commands::Tui => {
342 envx_tui::run()?;
344 }
345
346 Commands::Path {
347 action,
348 check,
349 var,
350 permanent,
351 } => {
352 handle_path_command(action, check, &var, permanent)?;
353 }
354
355 Commands::Export {
356 file,
357 vars,
358 format,
359 source,
360 metadata,
361 force,
362 } => {
363 handle_export(&file, &vars, format, source, metadata, force)?;
364 }
365
366 Commands::Import {
367 file,
368 vars,
369 format,
370 permanent,
371 prefix,
372 overwrite,
373 dry_run,
374 } => {
375 handle_import(&file, &vars, format, permanent, prefix.as_ref(), overwrite, dry_run)?;
376 }
377
378 Commands::Snapshot(args) => {
379 handle_snapshot(args)?;
380 }
381 Commands::Profile(args) => {
382 handle_profile(args)?;
383 }
384
385 Commands::Project(args) => {
386 handle_project(args)?;
387 }
388
389 Commands::Rename(args) => {
390 handle_rename(&args)?;
391 }
392
393 Commands::Replace(args) => {
394 handle_replace(&args)?;
395 }
396
397 Commands::FindReplace(args) => {
398 handle_find_replace(&args)?;
399 }
400
401 Commands::Watch(args) => {
402 handle_watch(&args)?;
403 }
404
405 Commands::Monitor(args) => {
406 handle_monitor(args)?;
407 }
408 }
409
410 Ok(())
411}
412
413fn handle_get_command(pattern: &str, format: &str) -> Result<()> {
414 let mut manager = EnvVarManager::new();
415 manager.load_all()?;
416
417 let vars = manager.get_pattern(pattern);
418
419 if vars.is_empty() {
420 eprintln!("No variables found matching pattern: {pattern}");
421 return Ok(());
422 }
423
424 match format {
425 "json" => {
426 println!("{}", serde_json::to_string_pretty(&vars)?);
427 }
428 "detailed" => {
429 for var in vars {
430 println!("Name: {}", var.name);
431 println!("Value: {}", var.value);
432 println!("Source: {:?}", var.source);
433 println!("Modified: {}", var.modified.format("%Y-%m-%d %H:%M:%S"));
434 if let Some(orig) = &var.original_value {
435 println!("Original: {orig}");
436 }
437 println!("---");
438 }
439 }
440 _ => {
441 for var in vars {
442 println!("{} = {}", var.name, var.value);
443 }
444 }
445 }
446 Ok(())
447}
448
449fn handle_set_command(name: &str, value: &str, temporary: bool) -> Result<()> {
450 let mut manager = EnvVarManager::new();
451 manager.load_all()?;
452
453 let permanent = !temporary;
454
455 manager.set(name, value, permanent)?;
456 if permanent {
457 println!("✅ Set {name} = \"{value}\"");
458 #[cfg(windows)]
459 println!("📝 Note: You may need to restart your terminal for changes to take effect");
460 } else {
461 println!("⚡ Set {name} = \"{value}\" (temporary - current session only)");
462 }
463 Ok(())
464}
465
466fn handle_delete_command(pattern: &str, force: bool) -> Result<()> {
467 let mut manager = EnvVarManager::new();
468 manager.load_all()?;
469
470 let vars_to_delete: Vec<String> = manager
472 .get_pattern(pattern)
473 .into_iter()
474 .map(|v| v.name.clone())
475 .collect();
476
477 if vars_to_delete.is_empty() {
478 eprintln!("No variables found matching pattern: {pattern}");
479 return Ok(());
480 }
481
482 if !force && vars_to_delete.len() > 1 {
483 println!("About to delete {} variables:", vars_to_delete.len());
484 for name in &vars_to_delete {
485 println!(" - {name}");
486 }
487 print!("Continue? [y/N]: ");
488 std::io::stdout().flush()?;
489
490 let mut input = String::new();
491 std::io::stdin().read_line(&mut input)?;
492
493 if !input.trim().eq_ignore_ascii_case("y") {
494 println!("Cancelled.");
495 return Ok(());
496 }
497 }
498
499 for name in vars_to_delete {
501 manager.delete(&name)?;
502 println!("Deleted: {name}");
503 }
504 Ok(())
505}
506
507fn handle_analyze_command(analysis_type: &str) -> Result<()> {
508 let mut manager = EnvVarManager::new();
509 manager.load_all()?;
510 let vars = manager.list().into_iter().cloned().collect();
511 let analyzer = Analyzer::new(vars);
512
513 match analysis_type {
514 "duplicates" | "all" => {
515 let duplicates = analyzer.find_duplicates();
516 if !duplicates.is_empty() {
517 println!("Duplicate variables found:");
518 for (name, vars) in duplicates {
519 println!(" {}: {} instances", name, vars.len());
520 }
521 }
522 }
523 "invalid" => {
524 let validation = analyzer.validate_all();
525 for (name, result) in validation {
526 if !result.valid {
527 println!("Invalid variable: {name}");
528 for error in result.errors {
529 println!(" Error: {error}");
530 }
531 }
532 }
533 }
534 _ => {}
535 }
536 Ok(())
537}
538
539fn handle_export(
540 file: &str,
541 vars: &[String],
542 format: Option<String>,
543 source: Option<String>,
544 metadata: bool,
545 force: bool,
546) -> Result<()> {
547 if Path::new(&file).exists() && !force {
549 print!("File '{file}' already exists. Overwrite? [y/N]: ");
550 std::io::stdout().flush()?;
551
552 let mut input = String::new();
553 std::io::stdin().read_line(&mut input)?;
554
555 if !input.trim().eq_ignore_ascii_case("y") {
556 println!("Export cancelled.");
557 return Ok(());
558 }
559 }
560
561 let mut manager = EnvVarManager::new();
563 manager.load_all()?;
564
565 let mut vars_to_export = if vars.is_empty() {
567 manager.list().into_iter().cloned().collect()
568 } else {
569 let mut selected = Vec::new();
570 for pattern in vars {
571 let matched = manager.get_pattern(pattern);
572 selected.extend(matched.into_iter().cloned());
573 }
574 selected
575 };
576
577 if let Some(src) = source {
579 let source_filter = match src.as_str() {
580 "system" => envx_core::EnvVarSource::System,
581 "user" => envx_core::EnvVarSource::User,
582 "process" => envx_core::EnvVarSource::Process,
583 "shell" => envx_core::EnvVarSource::Shell,
584 _ => return Err(eyre!("Invalid source: {}", src)),
585 };
586
587 vars_to_export.retain(|v| v.source == source_filter);
588 }
589
590 if vars_to_export.is_empty() {
591 println!("No variables to export.");
592 return Ok(());
593 }
594
595 let export_format = if let Some(fmt) = format {
597 match fmt.as_str() {
598 "env" => ExportFormat::DotEnv,
599 "json" => ExportFormat::Json,
600 "yaml" | "yml" => ExportFormat::Yaml,
601 "txt" | "text" => ExportFormat::Text,
602 "ps1" | "powershell" => ExportFormat::PowerShell,
603 "sh" | "bash" => ExportFormat::Shell,
604 _ => return Err(eyre!("Unsupported format: {}", fmt)),
605 }
606 } else {
607 ExportFormat::from_extension(file)?
609 };
610
611 let exporter = Exporter::new(vars_to_export, metadata);
613 exporter.export_to_file(file, export_format)?;
614
615 println!("Exported {} variables to '{}'", exporter.count(), file);
616
617 Ok(())
618}
619
620fn handle_import(
621 file: &str,
622 vars: &[String],
623 format: Option<String>,
624 permanent: bool,
625 prefix: Option<&String>,
626 overwrite: bool,
627 dry_run: bool,
628) -> Result<()> {
629 if !Path::new(&file).exists() {
631 return Err(eyre!("File not found: {}", file));
632 }
633
634 let import_format = if let Some(fmt) = format {
636 match fmt.as_str() {
637 "env" => ImportFormat::DotEnv,
638 "json" => ImportFormat::Json,
639 "yaml" | "yml" => ImportFormat::Yaml,
640 "txt" | "text" => ImportFormat::Text,
641 _ => return Err(eyre!("Unsupported format: {}", fmt)),
642 }
643 } else {
644 ImportFormat::from_extension(file)?
646 };
647
648 let mut importer = Importer::new();
650 importer.import_from_file(file, import_format)?;
651
652 if !vars.is_empty() {
654 importer.filter_by_patterns(vars);
655 }
656
657 if let Some(pfx) = &prefix {
659 importer.add_prefix(pfx);
660 }
661
662 let import_vars = importer.get_variables();
664
665 if import_vars.is_empty() {
666 println!("No variables to import.");
667 return Ok(());
668 }
669
670 let mut manager = EnvVarManager::new();
672 manager.load_all()?;
673
674 let mut conflicts = Vec::new();
675 for (name, _) in &import_vars {
676 if manager.get(name).is_some() {
677 conflicts.push(name.clone());
678 }
679 }
680
681 if !conflicts.is_empty() && !overwrite && !dry_run {
682 println!("The following variables already exist:");
683 for name in &conflicts {
684 println!(" - {name}");
685 }
686
687 print!("Overwrite existing variables? [y/N]: ");
688 std::io::stdout().flush()?;
689
690 let mut input = String::new();
691 std::io::stdin().read_line(&mut input)?;
692
693 if !input.trim().eq_ignore_ascii_case("y") {
694 println!("Import cancelled.");
695 return Ok(());
696 }
697 }
698
699 if dry_run {
701 println!("Would import {} variables:", import_vars.len());
702 for (name, value) in &import_vars {
703 let status = if conflicts.contains(name) {
704 " [OVERWRITE]"
705 } else {
706 " [NEW]"
707 };
708 println!(
709 " {} = {}{}",
710 name,
711 if value.len() > 50 {
712 format!("{}...", &value[..50])
713 } else {
714 value.clone()
715 },
716 status
717 );
718 }
719 println!("\n(Dry run - no changes made)");
720 } else {
721 let mut imported = 0;
723 let mut failed = 0;
724
725 for (name, value) in import_vars {
726 match manager.set(&name, &value, permanent) {
727 Ok(()) => imported += 1,
728 Err(e) => {
729 eprintln!("Failed to import {name}: {e}");
730 failed += 1;
731 }
732 }
733 }
734
735 println!("Imported {imported} variables");
736 if failed > 0 {
737 println!("Failed to import {failed} variables");
738 }
739 }
740
741 Ok(())
742}