envx_cli/
cli.rs

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 environment variables
43    List {
44        /// Filter by source (system, user, process, shell)
45        #[arg(short, long)]
46        source: Option<String>,
47
48        /// Search query
49        #[arg(short = 'q', long)]
50        query: Option<String>,
51
52        /// Output format (json, table, simple, compact)
53        #[arg(short, long, default_value = "table")]
54        format: String,
55
56        /// Sort by (name, value, source)
57        #[arg(long, default_value = "name")]
58        sort: String,
59
60        /// Show only variable names
61        #[arg(long)]
62        names_only: bool,
63
64        /// Limit output to N entries
65        #[arg(short, long)]
66        limit: Option<usize>,
67
68        /// Show statistics summary
69        #[arg(long)]
70        stats: bool,
71    },
72
73    /// Get a specific environment variable
74    Get {
75        /// Variable name or pattern (supports *, ?, and /regex/)
76        /// Examples:
77        ///   envx get PATH           - exact match
78        ///   envx get PATH*          - starts with PATH
79        ///   envx get *PATH          - ends with PATH
80        ///   envx get *PATH*         - contains PATH
81        ///   envx get P?TH           - P followed by any char, then TH
82        ///   envx get /^JAVA.*/      - regex pattern
83        pattern: String,
84
85        /// Output format (simple, detailed, json)
86        #[arg(short, long, default_value = "simple")]
87        format: String,
88    },
89
90    /// Set an environment variable
91    Set {
92        /// Variable name
93        name: String,
94
95        /// Variable value
96        value: String,
97
98        /// Set as temporary (only for current session)
99        #[arg(short, long)]
100        temporary: bool,
101    },
102
103    /// Delete environment variable(s)
104    Delete {
105        /// Variable name or pattern
106        pattern: String,
107
108        /// Force deletion without confirmation
109        #[arg(short, long)]
110        force: bool,
111    },
112
113    /// Analyze environment variables
114    Analyze {
115        /// Type of analysis (duplicates, invalid)
116        #[arg(short, long, default_value = "all")]
117        analysis_type: String,
118    },
119
120    /// Launch the TUI
121    #[command(visible_alias = "ui")]
122    Tui,
123
124    /// Manage PATH variable
125    Path {
126        #[command(subcommand)]
127        action: Option<PathAction>,
128
129        /// Check if all paths exist
130        #[arg(short, long)]
131        check: bool,
132
133        /// Target PATH variable (PATH, Path, or custom like PYTHONPATH)
134        #[arg(short = 'v', long, default_value = "PATH")]
135        var: String,
136
137        /// Apply changes permanently
138        #[arg(short = 'p', long)]
139        permanent: bool,
140    },
141
142    /// Export environment variables to a file
143    Export {
144        /// Output file path
145        file: String,
146
147        /// Variable names or patterns to export (exports all if not specified)
148        #[arg(short = 'v', long)]
149        vars: Vec<String>,
150
151        /// Export format (auto-detect from extension, or: env, json, yaml, txt)
152        #[arg(short, long)]
153        format: Option<String>,
154
155        /// Include only specific sources (system, user, process, shell)
156        #[arg(short, long)]
157        source: Option<String>,
158
159        /// Include metadata (source, modified time)
160        #[arg(short, long)]
161        metadata: bool,
162
163        /// Overwrite existing file without confirmation
164        #[arg(long)]
165        force: bool,
166    },
167
168    /// Import environment variables from a file
169    Import {
170        /// Input file path
171        file: String,
172
173        /// Variable names or patterns to import (imports all if not specified)
174        #[arg(short = 'v', long)]
175        vars: Vec<String>,
176
177        /// Import format (auto-detect from extension, or: env, json, yaml, txt)
178        #[arg(short, long)]
179        format: Option<String>,
180
181        /// Make imported variables permanent
182        #[arg(short, long)]
183        permanent: bool,
184
185        /// Prefix to add to all imported variable names
186        #[arg(long)]
187        prefix: Option<String>,
188
189        /// Overwrite existing variables without confirmation
190        #[arg(long)]
191        overwrite: bool,
192
193        /// Dry run - show what would be imported without making changes
194        #[arg(short = 'n', long)]
195        dry_run: bool,
196    },
197
198    /// Manage environment snapshots
199    Snapshot(SnapshotArgs),
200
201    /// Manage environment profiles
202    Profile(ProfileArgs),
203
204    /// Manage project-specific configuration
205    Project(ProjectArgs),
206
207    /// Rename environment variables (supports wildcards)
208    Rename(RenameArgs),
209
210    /// Replace environment variable values
211    Replace(ReplaceArgs),
212
213    /// Find and replace text within environment variable values
214    FindReplace(FindReplaceArgs),
215
216    /// Watch files for changes and auto-sync
217    Watch(WatchArgs),
218
219    /// Monitor environment variable changes (read-only)
220    Monitor(MonitorArgs),
221
222    /// Generate documentation for environment variables
223    Docs(DocsArgs),
224
225    /// Show environment variable dependencies
226    Deps(DepsArgs),
227
228    /// Remove unused environment variables
229    Cleanup(CleanupArgs),
230}
231
232#[derive(Subcommand)]
233pub enum PathAction {
234    /// Add a directory to PATH
235    Add {
236        /// Directory to add
237        directory: String,
238
239        /// Add to the beginning of PATH (highest priority)
240        #[arg(short, long)]
241        first: bool,
242
243        /// Create directory if it doesn't exist
244        #[arg(short, long)]
245        create: bool,
246    },
247
248    /// Remove a directory from PATH
249    Remove {
250        /// Directory to remove (supports wildcards)
251        directory: String,
252
253        /// Remove all occurrences (not just first)
254        #[arg(short, long)]
255        all: bool,
256    },
257
258    /// Clean invalid/non-existent entries from PATH
259    Clean {
260        /// Also remove duplicate entries
261        #[arg(short, long)]
262        dedupe: bool,
263
264        /// Dry run - show what would be removed without making changes
265        #[arg(short = 'n', long)]
266        dry_run: bool,
267    },
268
269    /// Remove duplicate entries from PATH
270    Dedupe {
271        /// Keep first occurrence (default is last)
272        #[arg(short, long)]
273        keep_first: bool,
274
275        /// Dry run - show what would be removed
276        #[arg(short = 'n', long)]
277        dry_run: bool,
278    },
279
280    /// Check PATH for issues
281    Check {
282        /// Verbose output
283        #[arg(short, long)]
284        verbose: bool,
285    },
286
287    /// Show PATH entries in order
288    List {
289        /// Show with index numbers
290        #[arg(short, long)]
291        numbered: bool,
292
293        /// Check existence of each path
294        #[arg(short, long)]
295        check: bool,
296    },
297
298    /// Move a PATH entry to a different position
299    Move {
300        /// Path or index to move
301        from: String,
302
303        /// Target position (first, last, or index)
304        to: String,
305    },
306}
307
308/// Execute the CLI command with the given arguments.
309///
310/// # Errors
311///
312/// This function will return an error if:
313/// - Environment variable operations fail (loading, setting, deleting)
314/// - File I/O operations fail (import/export)
315/// - User input cannot be read
316/// - Invalid command arguments are provided
317/// - TUI mode is requested (should be handled by main binary)
318pub 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            // Launch the TUI
358            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    // Collect the names to delete first (owned data, not references)
498    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    // Now we can safely delete since we're not holding any references to manager
527    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    // Check if file exists
575    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    // Load environment variables
589    let mut manager = EnvVarManager::new();
590    manager.load_all()?;
591
592    // Filter variables to export
593    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    // Filter by source if specified
605    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    // Determine format
623    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        // Auto-detect from extension
635        ExportFormat::from_extension(file)?
636    };
637
638    // Export
639    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    // Check if file exists
657    if !Path::new(&file).exists() {
658        return Err(eyre!("File not found: {}", file));
659    }
660
661    // Determine format
662    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        // Auto-detect from extension
672        ImportFormat::from_extension(file)?
673    };
674
675    // Import variables
676    let mut importer = Importer::new();
677    importer.import_from_file(file, import_format)?;
678
679    // Filter variables if patterns specified
680    if !vars.is_empty() {
681        importer.filter_by_patterns(vars);
682    }
683
684    // Add prefix if specified
685    if let Some(pfx) = &prefix {
686        importer.add_prefix(pfx);
687    }
688
689    // Get variables to import
690    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    // Check for conflicts
698    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    // Preview or apply changes
727    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        // Apply imports
749        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}