envx_cli/
cli.rs

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 environment variables
36    List {
37        /// Filter by source (system, user, process, shell)
38        #[arg(short, long)]
39        source: Option<String>,
40
41        /// Search query
42        #[arg(short = 'q', long)]
43        query: Option<String>,
44
45        /// Output format (json, table, simple, compact)
46        #[arg(short, long, default_value = "table")]
47        format: String,
48
49        /// Sort by (name, value, source)
50        #[arg(long, default_value = "name")]
51        sort: String,
52
53        /// Show only variable names
54        #[arg(long)]
55        names_only: bool,
56
57        /// Limit output to N entries
58        #[arg(short, long)]
59        limit: Option<usize>,
60
61        /// Show statistics summary
62        #[arg(long)]
63        stats: bool,
64    },
65
66    /// Get a specific environment variable
67    Get {
68        /// Variable name or pattern (supports *, ?, and /regex/)
69        /// Examples:
70        ///   envx get PATH           - exact match
71        ///   envx get PATH*          - starts with PATH
72        ///   envx get *PATH          - ends with PATH
73        ///   envx get *PATH*         - contains PATH
74        ///   envx get P?TH           - P followed by any char, then TH
75        ///   envx get /^JAVA.*/      - regex pattern
76        pattern: String,
77
78        /// Output format (simple, detailed, json)
79        #[arg(short, long, default_value = "simple")]
80        format: String,
81    },
82
83    /// Set an environment variable
84    Set {
85        /// Variable name
86        name: String,
87
88        /// Variable value
89        value: String,
90
91        /// Set as temporary (only for current session)
92        #[arg(short, long)]
93        temporary: bool,
94    },
95
96    /// Delete environment variable(s)
97    Delete {
98        /// Variable name or pattern
99        pattern: String,
100
101        /// Force deletion without confirmation
102        #[arg(short, long)]
103        force: bool,
104    },
105
106    /// Analyze environment variables
107    Analyze {
108        /// Type of analysis (duplicates, invalid)
109        #[arg(short, long, default_value = "all")]
110        analysis_type: String,
111    },
112
113    /// Launch the TUI
114    #[command(visible_alias = "ui")]
115    Tui,
116
117    /// Manage PATH variable
118    Path {
119        #[command(subcommand)]
120        action: Option<PathAction>,
121
122        /// Check if all paths exist
123        #[arg(short, long)]
124        check: bool,
125
126        /// Target PATH variable (PATH, Path, or custom like PYTHONPATH)
127        #[arg(short = 'v', long, default_value = "PATH")]
128        var: String,
129
130        /// Apply changes permanently
131        #[arg(short = 'p', long)]
132        permanent: bool,
133    },
134
135    /// Export environment variables to a file
136    Export {
137        /// Output file path
138        file: String,
139
140        /// Variable names or patterns to export (exports all if not specified)
141        #[arg(short = 'v', long)]
142        vars: Vec<String>,
143
144        /// Export format (auto-detect from extension, or: env, json, yaml, txt)
145        #[arg(short, long)]
146        format: Option<String>,
147
148        /// Include only specific sources (system, user, process, shell)
149        #[arg(short, long)]
150        source: Option<String>,
151
152        /// Include metadata (source, modified time)
153        #[arg(short, long)]
154        metadata: bool,
155
156        /// Overwrite existing file without confirmation
157        #[arg(long)]
158        force: bool,
159    },
160
161    /// Import environment variables from a file
162    Import {
163        /// Input file path
164        file: String,
165
166        /// Variable names or patterns to import (imports all if not specified)
167        #[arg(short = 'v', long)]
168        vars: Vec<String>,
169
170        /// Import format (auto-detect from extension, or: env, json, yaml, txt)
171        #[arg(short, long)]
172        format: Option<String>,
173
174        /// Make imported variables permanent
175        #[arg(short, long)]
176        permanent: bool,
177
178        /// Prefix to add to all imported variable names
179        #[arg(long)]
180        prefix: Option<String>,
181
182        /// Overwrite existing variables without confirmation
183        #[arg(long)]
184        overwrite: bool,
185
186        /// Dry run - show what would be imported without making changes
187        #[arg(short = 'n', long)]
188        dry_run: bool,
189    },
190
191    /// Manage environment snapshots
192    Snapshot(SnapshotArgs),
193
194    /// Manage environment profiles
195    Profile(ProfileArgs),
196
197    /// Manage project-specific configuration
198    Project(ProjectArgs),
199
200    /// Rename environment variables (supports wildcards)
201    Rename(RenameArgs),
202
203    /// Replace environment variable values
204    Replace(ReplaceArgs),
205
206    /// Find and replace text within environment variable values
207    FindReplace(FindReplaceArgs),
208}
209
210#[derive(Subcommand)]
211pub enum PathAction {
212    /// Add a directory to PATH
213    Add {
214        /// Directory to add
215        directory: String,
216
217        /// Add to the beginning of PATH (highest priority)
218        #[arg(short, long)]
219        first: bool,
220
221        /// Create directory if it doesn't exist
222        #[arg(short, long)]
223        create: bool,
224    },
225
226    /// Remove a directory from PATH
227    Remove {
228        /// Directory to remove (supports wildcards)
229        directory: String,
230
231        /// Remove all occurrences (not just first)
232        #[arg(short, long)]
233        all: bool,
234    },
235
236    /// Clean invalid/non-existent entries from PATH
237    Clean {
238        /// Also remove duplicate entries
239        #[arg(short, long)]
240        dedupe: bool,
241
242        /// Dry run - show what would be removed without making changes
243        #[arg(short = 'n', long)]
244        dry_run: bool,
245    },
246
247    /// Remove duplicate entries from PATH
248    Dedupe {
249        /// Keep first occurrence (default is last)
250        #[arg(short, long)]
251        keep_first: bool,
252
253        /// Dry run - show what would be removed
254        #[arg(short = 'n', long)]
255        dry_run: bool,
256    },
257
258    /// Check PATH for issues
259    Check {
260        /// Verbose output
261        #[arg(short, long)]
262        verbose: bool,
263    },
264
265    /// Show PATH entries in order
266    List {
267        /// Show with index numbers
268        #[arg(short, long)]
269        numbered: bool,
270
271        /// Check existence of each path
272        #[arg(short, long)]
273        check: bool,
274    },
275
276    /// Move a PATH entry to a different position
277    Move {
278        /// Path or index to move
279        from: String,
280
281        /// Target position (first, last, or index)
282        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 a new snapshot
295    Create {
296        /// Snapshot name
297        name: String,
298        /// Description
299        #[arg(short, long)]
300        description: Option<String>,
301    },
302    /// List all snapshots
303    List,
304    /// Show details of a snapshot
305    Show {
306        /// Snapshot name or ID
307        snapshot: String,
308    },
309    /// Restore from a snapshot
310    Restore {
311        /// Snapshot name or ID
312        snapshot: String,
313        /// Force restore without confirmation
314        #[arg(short, long)]
315        force: bool,
316    },
317    /// Delete a snapshot
318    Delete {
319        /// Snapshot name or ID
320        snapshot: String,
321        /// Force deletion without confirmation
322        #[arg(short, long)]
323        force: bool,
324    },
325    /// Compare two snapshots
326    Diff {
327        /// First snapshot
328        snapshot1: String,
329        /// Second snapshot
330        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 a new profile
343    Create {
344        /// Profile name
345        name: String,
346        /// Description
347        #[arg(short, long)]
348        description: Option<String>,
349    },
350    /// List all profiles
351    List,
352    /// Show current or specific profile
353    Show {
354        /// Profile name (shows active if not specified)
355        name: Option<String>,
356    },
357    /// Switch to a profile
358    Switch {
359        /// Profile name
360        name: String,
361        /// Apply immediately
362        #[arg(short, long)]
363        apply: bool,
364    },
365    /// Add a variable to a profile
366    Add {
367        /// Profile name
368        profile: String,
369        /// Variable name
370        name: String,
371        /// Variable value
372        value: String,
373        /// Override system variable
374        #[arg(short, long)]
375        override_system: bool,
376    },
377    /// Remove a variable from a profile
378    Remove {
379        /// Profile name
380        profile: String,
381        /// Variable name
382        name: String,
383    },
384    /// Delete a profile
385    Delete {
386        /// Profile name
387        name: String,
388        /// Force deletion
389        #[arg(short, long)]
390        force: bool,
391    },
392    /// Export a profile
393    Export {
394        /// Profile name
395        name: String,
396        /// Output file (stdout if not specified)
397        #[arg(short, long)]
398        output: Option<String>,
399    },
400    /// Import a profile
401    Import {
402        /// Import file
403        file: String,
404        /// Profile name
405        #[arg(short, long)]
406        name: Option<String>,
407        /// Overwrite if exists
408        #[arg(short, long)]
409        overwrite: bool,
410    },
411    /// Apply a profile to current environment
412    Apply {
413        /// Profile name
414        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    /// Initialize a new project configuration
427    Init {
428        /// Project name
429        #[arg(short, long)]
430        name: Option<String>,
431    },
432    /// Apply project configuration
433    Apply {
434        /// Force apply even with validation errors
435        #[arg(short, long)]
436        force: bool,
437    },
438    /// Validate project configuration
439    Check,
440    /// Edit project configuration
441    Edit,
442    /// Show project information
443    Info,
444    /// Run a project script
445    Run {
446        /// Script name
447        script: String,
448    },
449    /// Add a required variable
450    Require {
451        /// Variable name
452        name: String,
453        /// Description
454        #[arg(short, long)]
455        description: Option<String>,
456        /// Validation pattern (regex)
457        #[arg(short, long)]
458        pattern: Option<String>,
459        /// Example value
460        #[arg(short, long)]
461        example: Option<String>,
462    },
463}
464
465#[derive(Args)]
466pub struct RenameArgs {
467    /// Pattern to match (supports wildcards with *)
468    pub pattern: String,
469
470    /// New name or pattern
471    pub replacement: String,
472
473    /// Dry run - show what would be renamed without making changes
474    #[arg(long)]
475    pub dry_run: bool,
476}
477
478#[derive(Args)]
479pub struct ReplaceArgs {
480    /// Variable name or pattern (supports wildcards with *)
481    pub pattern: String,
482
483    /// New value to set
484    pub value: String,
485
486    /// Dry run - show what would be replaced without making changes
487    #[arg(long)]
488    pub dry_run: bool,
489}
490
491#[derive(Args)]
492pub struct FindReplaceArgs {
493    /// Text to search for in values
494    pub search: String,
495
496    /// Text to replace with
497    pub replacement: String,
498
499    /// Only search in variables matching this pattern (supports wildcards)
500    #[arg(short = 'p', long)]
501    pub pattern: Option<String>,
502
503    /// Dry run - show what would be replaced without making changes
504    #[arg(long)]
505    pub dry_run: bool,
506}
507
508/// Execute the CLI command with the given arguments.
509///
510/// # Errors
511///
512/// This function will return an error if:
513/// - Environment variable operations fail (loading, setting, deleting)
514/// - File I/O operations fail (import/export)
515/// - User input cannot be read
516/// - Invalid command arguments are provided
517/// - TUI mode is requested (should be handled by main binary)
518pub 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            // Launch the TUI
558            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    // Collect the names to delete first (owned data, not references)
678    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    // Now we can safely delete since we're not holding any references to manager
707    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    // Get the PATH variable
752    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 no action specified, list PATH entries
757    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            // Check if directory exists
774            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            // Check if already in PATH
793            if path_mgr.contains(&directory) {
794                println!("Directory already in {var}: {directory}");
795                return Ok(());
796            }
797
798            // Add to PATH
799            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            // Save changes
808            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) // Keep last by default
857                } 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            // Parse from (can be index or path)
904            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            // Parse to (can be "first", "last", or index)
913            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    // Summary
961    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    // Check if file exists
1031    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    // Load environment variables
1045    let mut manager = EnvVarManager::new();
1046    manager.load_all()?;
1047
1048    // Filter variables to export
1049    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    // Filter by source if specified
1061    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    // Determine format
1079    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        // Auto-detect from extension
1091        ExportFormat::from_extension(file)?
1092    };
1093
1094    // Export
1095    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    // Check if file exists
1113    if !Path::new(&file).exists() {
1114        return Err(eyre!("File not found: {}", file));
1115    }
1116
1117    // Determine format
1118    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        // Auto-detect from extension
1128        ImportFormat::from_extension(file)?
1129    };
1130
1131    // Import variables
1132    let mut importer = Importer::new();
1133    importer.import_from_file(file, import_format)?;
1134
1135    // Filter variables if patterns specified
1136    if !vars.is_empty() {
1137        importer.filter_by_patterns(vars);
1138    }
1139
1140    // Add prefix if specified
1141    if let Some(pfx) = &prefix {
1142        importer.add_prefix(pfx);
1143    }
1144
1145    // Get variables to import
1146    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    // Check for conflicts
1154    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    // Preview or apply changes
1183    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        // Apply imports
1205        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    // Get filtered variables
1240    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    // Sort variables
1256    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    // Apply limit if specified
1264    let total_count = vars.len();
1265    if let Some(lim) = limit {
1266        vars.truncate(lim);
1267    }
1268
1269    // Show statistics if requested
1270    if stats || (format == "table" && !names_only) {
1271        print_statistics(&manager, &vars, total_count, query, source);
1272    }
1273
1274    // Handle names_only flag
1275    if names_only {
1276        for var in vars {
1277            println!("{}", var.name);
1278        }
1279        return Ok(());
1280    }
1281
1282    // Format output
1283    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    // Show limit notice
1309    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
1324/// Handle snapshot-related commands.
1325///
1326/// # Errors
1327///
1328/// This function will return an error if:
1329/// - The snapshot manager cannot be initialized
1330/// - Environment variable loading fails
1331/// - Snapshot operations fail (create, restore, delete, etc.)
1332/// - File I/O operations fail during snapshot operations
1333/// - User input cannot be read from stdin
1334/// - Invalid snapshot names or IDs are provided
1335pub 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            // Show first 10 variables
1377            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
1454/// Handle profile-related commands.
1455///
1456/// # Errors
1457///
1458/// This function will return an error if:
1459/// - The profile manager cannot be initialized
1460/// - Environment variable loading fails  
1461/// - Profile operations fail (create, switch, delete, etc.)
1462/// - File I/O operations fail during profile import/export
1463/// - User input cannot be read from stdin
1464/// - Invalid profile names are provided
1465/// - Profile data cannot be serialized/deserialized
1466pub 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    // Count by source
1686    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    // Header
1692    println!("{}", style("═".repeat(60)).blue().bold());
1693    println!("{}", style("Environment Variables Summary").cyan().bold());
1694    println!("{}", style("═".repeat(60)).blue().bold());
1695
1696    // Filter info
1697    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    // Source breakdown with visual bars
1724    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    // Configure table style
1768    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    // Add rows with colored source indicators
1778    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/// Handle project-related commands.
1823///
1824/// # Errors
1825///
1826/// This function will return an error if:
1827/// - Project manager initialization fails
1828/// - Environment variable manager operations fail
1829/// - Project configuration file cannot be found, read, or written
1830/// - Project validation fails (when not using --force)
1831/// - Profile manager operations fail
1832/// - Script execution fails
1833/// - Required variable configuration cannot be updated
1834/// - File I/O operations fail during project operations
1835#[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                // Validate first
1852                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                // Apply configuration
1862                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                // Load and display config
1923                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            // Load, modify, and save config
1961            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
2007/// Handle rename command to rename environment variables using patterns.
2008///
2009/// # Errors
2010///
2011/// This function will return an error if:
2012/// - Environment variable operations fail (loading, renaming)
2013/// - Pattern matching fails or produces invalid results
2014/// - Variable names conflict or are invalid
2015/// - File I/O operations fail when persisting changes
2016/// - User input cannot be read from stdin during confirmation
2017pub fn handle_rename(args: &RenameArgs) -> Result<()> {
2018    let mut manager = EnvVarManager::new();
2019    manager.load_all()?;
2020
2021    if args.dry_run {
2022        // Show what would be renamed
2023        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
2091/// Handle replace command to replace environment variable values using patterns.
2092///
2093/// # Errors
2094///
2095/// This function will return an error if:
2096/// - Environment variable operations fail (loading, replacing)
2097/// - Pattern matching fails or produces invalid results
2098/// - File I/O operations fail when persisting changes
2099/// - Wildcard pattern parsing fails
2100pub fn handle_replace(args: &ReplaceArgs) -> Result<()> {
2101    let mut manager = EnvVarManager::new();
2102    manager.load_all()?;
2103
2104    if args.dry_run {
2105        // Show what would be replaced
2106        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                // Truncate long values for display
2138                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
2182/// Handle find and replace operations within environment variable values.
2183///
2184/// # Errors
2185///
2186/// This function will return an error if:
2187/// - Environment variable operations fail (loading, updating)
2188/// - Pattern matching fails or produces invalid results
2189/// - Find and replace operations fail
2190/// - File I/O operations fail when persisting changes
2191/// - Wildcard pattern parsing fails
2192pub fn handle_find_replace(args: &FindReplaceArgs) -> Result<()> {
2193    let mut manager = EnvVarManager::new();
2194    manager.load_all()?;
2195
2196    if args.dry_run {
2197        // Show preview
2198        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                // Truncate long values
2230                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        // Check if variable matches pattern (if specified)
2263        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}