envx_cli/
cli.rs

1use crate::MonitorArgs;
2use crate::monitor::handle_monitor;
3use clap::Args;
4use clap::ValueEnum;
5use clap::{Parser, Subcommand};
6use color_eyre::Result;
7use color_eyre::eyre::eyre;
8use comfy_table::Attribute;
9use comfy_table::Cell;
10use comfy_table::Color;
11use comfy_table::ContentArrangement;
12use comfy_table::Table;
13use comfy_table::presets::UTF8_FULL;
14use console::Term;
15use console::style;
16use envx_core::ConflictStrategy;
17use envx_core::EnvWatcher;
18use envx_core::PathManager;
19use envx_core::ProjectConfig;
20use envx_core::ProjectManager;
21use envx_core::RequiredVar;
22use envx_core::SyncMode;
23use envx_core::ValidationReport;
24use envx_core::WatchConfig;
25use envx_core::env::split_wildcard_pattern;
26use envx_core::profile_manager::ProfileManager;
27use envx_core::snapshot_manager::SnapshotManager;
28use envx_core::{Analyzer, EnvVarManager, ExportFormat, Exporter, ImportFormat, Importer};
29use std::io::Write;
30use std::path::Path;
31use std::path::PathBuf;
32use std::time::Duration;
33#[derive(Parser)]
34#[command(name = "envx")]
35#[command(about = "System Environment Variable Manager")]
36#[command(version)]
37pub struct Cli {
38    #[command(subcommand)]
39    pub command: Commands,
40}
41
42#[derive(Subcommand)]
43pub enum Commands {
44    /// List environment variables
45    List {
46        /// Filter by source (system, user, process, shell)
47        #[arg(short, long)]
48        source: Option<String>,
49
50        /// Search query
51        #[arg(short = 'q', long)]
52        query: Option<String>,
53
54        /// Output format (json, table, simple, compact)
55        #[arg(short, long, default_value = "table")]
56        format: String,
57
58        /// Sort by (name, value, source)
59        #[arg(long, default_value = "name")]
60        sort: String,
61
62        /// Show only variable names
63        #[arg(long)]
64        names_only: bool,
65
66        /// Limit output to N entries
67        #[arg(short, long)]
68        limit: Option<usize>,
69
70        /// Show statistics summary
71        #[arg(long)]
72        stats: bool,
73    },
74
75    /// Get a specific environment variable
76    Get {
77        /// Variable name or pattern (supports *, ?, and /regex/)
78        /// Examples:
79        ///   envx get PATH           - exact match
80        ///   envx get PATH*          - starts with PATH
81        ///   envx get *PATH          - ends with PATH
82        ///   envx get *PATH*         - contains PATH
83        ///   envx get P?TH           - P followed by any char, then TH
84        ///   envx get /^JAVA.*/      - regex pattern
85        pattern: String,
86
87        /// Output format (simple, detailed, json)
88        #[arg(short, long, default_value = "simple")]
89        format: String,
90    },
91
92    /// Set an environment variable
93    Set {
94        /// Variable name
95        name: String,
96
97        /// Variable value
98        value: String,
99
100        /// Set as temporary (only for current session)
101        #[arg(short, long)]
102        temporary: bool,
103    },
104
105    /// Delete environment variable(s)
106    Delete {
107        /// Variable name or pattern
108        pattern: String,
109
110        /// Force deletion without confirmation
111        #[arg(short, long)]
112        force: bool,
113    },
114
115    /// Analyze environment variables
116    Analyze {
117        /// Type of analysis (duplicates, invalid)
118        #[arg(short, long, default_value = "all")]
119        analysis_type: String,
120    },
121
122    /// Launch the TUI
123    #[command(visible_alias = "ui")]
124    Tui,
125
126    /// Manage PATH variable
127    Path {
128        #[command(subcommand)]
129        action: Option<PathAction>,
130
131        /// Check if all paths exist
132        #[arg(short, long)]
133        check: bool,
134
135        /// Target PATH variable (PATH, Path, or custom like PYTHONPATH)
136        #[arg(short = 'v', long, default_value = "PATH")]
137        var: String,
138
139        /// Apply changes permanently
140        #[arg(short = 'p', long)]
141        permanent: bool,
142    },
143
144    /// Export environment variables to a file
145    Export {
146        /// Output file path
147        file: String,
148
149        /// Variable names or patterns to export (exports all if not specified)
150        #[arg(short = 'v', long)]
151        vars: Vec<String>,
152
153        /// Export format (auto-detect from extension, or: env, json, yaml, txt)
154        #[arg(short, long)]
155        format: Option<String>,
156
157        /// Include only specific sources (system, user, process, shell)
158        #[arg(short, long)]
159        source: Option<String>,
160
161        /// Include metadata (source, modified time)
162        #[arg(short, long)]
163        metadata: bool,
164
165        /// Overwrite existing file without confirmation
166        #[arg(long)]
167        force: bool,
168    },
169
170    /// Import environment variables from a file
171    Import {
172        /// Input file path
173        file: String,
174
175        /// Variable names or patterns to import (imports all if not specified)
176        #[arg(short = 'v', long)]
177        vars: Vec<String>,
178
179        /// Import format (auto-detect from extension, or: env, json, yaml, txt)
180        #[arg(short, long)]
181        format: Option<String>,
182
183        /// Make imported variables permanent
184        #[arg(short, long)]
185        permanent: bool,
186
187        /// Prefix to add to all imported variable names
188        #[arg(long)]
189        prefix: Option<String>,
190
191        /// Overwrite existing variables without confirmation
192        #[arg(long)]
193        overwrite: bool,
194
195        /// Dry run - show what would be imported without making changes
196        #[arg(short = 'n', long)]
197        dry_run: bool,
198    },
199
200    /// Manage environment snapshots
201    Snapshot(SnapshotArgs),
202
203    /// Manage environment profiles
204    Profile(ProfileArgs),
205
206    /// Manage project-specific configuration
207    Project(ProjectArgs),
208
209    /// Rename environment variables (supports wildcards)
210    Rename(RenameArgs),
211
212    /// Replace environment variable values
213    Replace(ReplaceArgs),
214
215    /// Find and replace text within environment variable values
216    FindReplace(FindReplaceArgs),
217
218    /// Watch files for changes and auto-sync
219    Watch(WatchArgs),
220
221    /// Monitor environment variable changes (read-only)
222    Monitor(MonitorArgs),
223}
224
225#[derive(Subcommand)]
226pub enum PathAction {
227    /// Add a directory to PATH
228    Add {
229        /// Directory to add
230        directory: String,
231
232        /// Add to the beginning of PATH (highest priority)
233        #[arg(short, long)]
234        first: bool,
235
236        /// Create directory if it doesn't exist
237        #[arg(short, long)]
238        create: bool,
239    },
240
241    /// Remove a directory from PATH
242    Remove {
243        /// Directory to remove (supports wildcards)
244        directory: String,
245
246        /// Remove all occurrences (not just first)
247        #[arg(short, long)]
248        all: bool,
249    },
250
251    /// Clean invalid/non-existent entries from PATH
252    Clean {
253        /// Also remove duplicate entries
254        #[arg(short, long)]
255        dedupe: bool,
256
257        /// Dry run - show what would be removed without making changes
258        #[arg(short = 'n', long)]
259        dry_run: bool,
260    },
261
262    /// Remove duplicate entries from PATH
263    Dedupe {
264        /// Keep first occurrence (default is last)
265        #[arg(short, long)]
266        keep_first: bool,
267
268        /// Dry run - show what would be removed
269        #[arg(short = 'n', long)]
270        dry_run: bool,
271    },
272
273    /// Check PATH for issues
274    Check {
275        /// Verbose output
276        #[arg(short, long)]
277        verbose: bool,
278    },
279
280    /// Show PATH entries in order
281    List {
282        /// Show with index numbers
283        #[arg(short, long)]
284        numbered: bool,
285
286        /// Check existence of each path
287        #[arg(short, long)]
288        check: bool,
289    },
290
291    /// Move a PATH entry to a different position
292    Move {
293        /// Path or index to move
294        from: String,
295
296        /// Target position (first, last, or index)
297        to: String,
298    },
299}
300
301#[derive(Args)]
302pub struct SnapshotArgs {
303    #[command(subcommand)]
304    pub command: SnapshotCommands,
305}
306
307#[derive(Subcommand)]
308pub enum SnapshotCommands {
309    /// Create a new snapshot
310    Create {
311        /// Snapshot name
312        name: String,
313        /// Description
314        #[arg(short, long)]
315        description: Option<String>,
316    },
317    /// List all snapshots
318    List,
319    /// Show details of a snapshot
320    Show {
321        /// Snapshot name or ID
322        snapshot: String,
323    },
324    /// Restore from a snapshot
325    Restore {
326        /// Snapshot name or ID
327        snapshot: String,
328        /// Force restore without confirmation
329        #[arg(short, long)]
330        force: bool,
331    },
332    /// Delete a snapshot
333    Delete {
334        /// Snapshot name or ID
335        snapshot: String,
336        /// Force deletion without confirmation
337        #[arg(short, long)]
338        force: bool,
339    },
340    /// Compare two snapshots
341    Diff {
342        /// First snapshot
343        snapshot1: String,
344        /// Second snapshot
345        snapshot2: String,
346    },
347}
348
349#[derive(Args)]
350pub struct ProfileArgs {
351    #[command(subcommand)]
352    pub command: ProfileCommands,
353}
354
355#[derive(Subcommand)]
356pub enum ProfileCommands {
357    /// Create a new profile
358    Create {
359        /// Profile name
360        name: String,
361        /// Description
362        #[arg(short, long)]
363        description: Option<String>,
364    },
365    /// List all profiles
366    List,
367    /// Show current or specific profile
368    Show {
369        /// Profile name (shows active if not specified)
370        name: Option<String>,
371    },
372    /// Switch to a profile
373    Switch {
374        /// Profile name
375        name: String,
376        /// Apply immediately
377        #[arg(short, long)]
378        apply: bool,
379    },
380    /// Add a variable to a profile
381    Add {
382        /// Profile name
383        profile: String,
384        /// Variable name
385        name: String,
386        /// Variable value
387        value: String,
388        /// Override system variable
389        #[arg(short, long)]
390        override_system: bool,
391    },
392    /// Remove a variable from a profile
393    Remove {
394        /// Profile name
395        profile: String,
396        /// Variable name
397        name: String,
398    },
399    /// Delete a profile
400    Delete {
401        /// Profile name
402        name: String,
403        /// Force deletion
404        #[arg(short, long)]
405        force: bool,
406    },
407    /// Export a profile
408    Export {
409        /// Profile name
410        name: String,
411        /// Output file (stdout if not specified)
412        #[arg(short, long)]
413        output: Option<String>,
414    },
415    /// Import a profile
416    Import {
417        /// Import file
418        file: String,
419        /// Profile name
420        #[arg(short, long)]
421        name: Option<String>,
422        /// Overwrite if exists
423        #[arg(short, long)]
424        overwrite: bool,
425    },
426    /// Apply a profile to current environment
427    Apply {
428        /// Profile name
429        name: String,
430    },
431}
432
433#[derive(Args)]
434pub struct ProjectArgs {
435    #[command(subcommand)]
436    pub command: ProjectCommands,
437}
438
439#[derive(Subcommand)]
440pub enum ProjectCommands {
441    /// Initialize a new project configuration
442    Init {
443        /// Project name
444        #[arg(short, long)]
445        name: Option<String>,
446    },
447    /// Apply project configuration
448    Apply {
449        /// Force apply even with validation errors
450        #[arg(short, long)]
451        force: bool,
452    },
453    /// Validate project configuration
454    Check,
455    /// Edit project configuration
456    Edit,
457    /// Show project information
458    Info,
459    /// Run a project script
460    Run {
461        /// Script name
462        script: String,
463    },
464    /// Add a required variable
465    Require {
466        /// Variable name
467        name: String,
468        /// Description
469        #[arg(short, long)]
470        description: Option<String>,
471        /// Validation pattern (regex)
472        #[arg(short, long)]
473        pattern: Option<String>,
474        /// Example value
475        #[arg(short, long)]
476        example: Option<String>,
477    },
478}
479
480#[derive(Args)]
481pub struct RenameArgs {
482    /// Pattern to match (supports wildcards with *)
483    pub pattern: String,
484
485    /// New name or pattern
486    pub replacement: String,
487
488    /// Dry run - show what would be renamed without making changes
489    #[arg(long)]
490    pub dry_run: bool,
491}
492
493#[derive(Args)]
494pub struct ReplaceArgs {
495    /// Variable name or pattern (supports wildcards with *)
496    pub pattern: String,
497
498    /// New value to set
499    pub value: String,
500
501    /// Dry run - show what would be replaced without making changes
502    #[arg(long)]
503    pub dry_run: bool,
504}
505
506#[derive(Args)]
507pub struct FindReplaceArgs {
508    /// Text to search for in values
509    pub search: String,
510
511    /// Text to replace with
512    pub replacement: String,
513
514    /// Only search in variables matching this pattern (supports wildcards)
515    #[arg(short = 'p', long)]
516    pub pattern: Option<String>,
517
518    /// Dry run - show what would be replaced without making changes
519    #[arg(long)]
520    pub dry_run: bool,
521}
522
523#[derive(Debug, Clone, ValueEnum)]
524pub enum Direction {
525    /// Sync from files to system (default)
526    FileToSystem,
527    /// Sync from system to files
528    SystemToFile,
529    /// Bidirectional synchronization
530    Bidirectional,
531}
532
533#[derive(Args, Clone)]
534pub struct WatchArgs {
535    /// Files or directories to watch (defaults to current directory)
536    #[arg(value_name = "PATH")]
537    pub paths: Vec<PathBuf>,
538
539    /// Sync direction
540    #[arg(short, long, value_enum, default_value = "file-to-system")]
541    pub direction: Direction,
542
543    /// Output file for system-to-file sync
544    #[arg(short, long)]
545    pub output: Option<PathBuf>,
546
547    /// File patterns to watch
548    #[arg(short, long)]
549    pub pattern: Vec<String>,
550
551    /// Debounce duration in milliseconds
552    #[arg(long, default_value = "300")]
553    pub debounce: u64,
554
555    /// Log changes to file
556    #[arg(short, long)]
557    pub log: Option<PathBuf>,
558
559    /// Variables to sync (sync all if not specified)
560    #[arg(short, long)]
561    pub vars: Vec<String>,
562
563    /// Quiet mode - less output
564    #[arg(short, long)]
565    pub quiet: bool,
566}
567
568/// Execute the CLI command with the given arguments.
569///
570/// # Errors
571///
572/// This function will return an error if:
573/// - Environment variable operations fail (loading, setting, deleting)
574/// - File I/O operations fail (import/export)
575/// - User input cannot be read
576/// - Invalid command arguments are provided
577/// - TUI mode is requested (should be handled by main binary)
578pub fn execute(cli: Cli) -> Result<()> {
579    match cli.command {
580        Commands::List {
581            source,
582            query,
583            format,
584            sort,
585            names_only,
586            limit,
587            stats,
588        } => {
589            handle_list_command(
590                source.as_deref(),
591                query.as_deref(),
592                &format,
593                &sort,
594                names_only,
595                limit,
596                stats,
597            )?;
598        }
599
600        Commands::Get { pattern, format } => {
601            handle_get_command(&pattern, &format)?;
602        }
603
604        Commands::Set { name, value, temporary } => {
605            handle_set_command(&name, &value, temporary)?;
606        }
607
608        Commands::Delete { pattern, force } => {
609            handle_delete_command(&pattern, force)?;
610        }
611
612        Commands::Analyze { analysis_type } => {
613            handle_analyze_command(&analysis_type)?;
614        }
615
616        Commands::Tui => {
617            // Launch the TUI
618            envx_tui::run()?;
619        }
620
621        Commands::Path {
622            action,
623            check,
624            var,
625            permanent,
626        } => {
627            handle_path_command(action, check, &var, permanent)?;
628        }
629
630        Commands::Export {
631            file,
632            vars,
633            format,
634            source,
635            metadata,
636            force,
637        } => {
638            handle_export(&file, &vars, format, source, metadata, force)?;
639        }
640
641        Commands::Import {
642            file,
643            vars,
644            format,
645            permanent,
646            prefix,
647            overwrite,
648            dry_run,
649        } => {
650            handle_import(&file, &vars, format, permanent, prefix.as_ref(), overwrite, dry_run)?;
651        }
652
653        Commands::Snapshot(args) => {
654            handle_snapshot(args)?;
655        }
656        Commands::Profile(args) => {
657            handle_profile(args)?;
658        }
659
660        Commands::Project(args) => {
661            handle_project(args)?;
662        }
663
664        Commands::Rename(args) => {
665            handle_rename(&args)?;
666        }
667
668        Commands::Replace(args) => {
669            handle_replace(&args)?;
670        }
671
672        Commands::FindReplace(args) => {
673            handle_find_replace(&args)?;
674        }
675
676        Commands::Watch(args) => {
677            handle_watch(&args)?;
678        }
679
680        Commands::Monitor(args) => {
681            handle_monitor(args)?;
682        }
683    }
684
685    Ok(())
686}
687
688fn handle_get_command(pattern: &str, format: &str) -> Result<()> {
689    let mut manager = EnvVarManager::new();
690    manager.load_all()?;
691
692    let vars = manager.get_pattern(pattern);
693
694    if vars.is_empty() {
695        eprintln!("No variables found matching pattern: {pattern}");
696        return Ok(());
697    }
698
699    match format {
700        "json" => {
701            println!("{}", serde_json::to_string_pretty(&vars)?);
702        }
703        "detailed" => {
704            for var in vars {
705                println!("Name: {}", var.name);
706                println!("Value: {}", var.value);
707                println!("Source: {:?}", var.source);
708                println!("Modified: {}", var.modified.format("%Y-%m-%d %H:%M:%S"));
709                if let Some(orig) = &var.original_value {
710                    println!("Original: {orig}");
711                }
712                println!("---");
713            }
714        }
715        _ => {
716            for var in vars {
717                println!("{} = {}", var.name, var.value);
718            }
719        }
720    }
721    Ok(())
722}
723
724fn handle_set_command(name: &str, value: &str, temporary: bool) -> Result<()> {
725    let mut manager = EnvVarManager::new();
726    manager.load_all()?;
727
728    let permanent = !temporary;
729
730    manager.set(name, value, permanent)?;
731    if permanent {
732        println!("āœ… Set {name} = \"{value}\"");
733        #[cfg(windows)]
734        println!("šŸ“ Note: You may need to restart your terminal for changes to take effect");
735    } else {
736        println!("⚔ Set {name} = \"{value}\" (temporary - current session only)");
737    }
738    Ok(())
739}
740
741fn handle_delete_command(pattern: &str, force: bool) -> Result<()> {
742    let mut manager = EnvVarManager::new();
743    manager.load_all()?;
744
745    // Collect the names to delete first (owned data, not references)
746    let vars_to_delete: Vec<String> = manager
747        .get_pattern(pattern)
748        .into_iter()
749        .map(|v| v.name.clone())
750        .collect();
751
752    if vars_to_delete.is_empty() {
753        eprintln!("No variables found matching pattern: {pattern}");
754        return Ok(());
755    }
756
757    if !force && vars_to_delete.len() > 1 {
758        println!("About to delete {} variables:", vars_to_delete.len());
759        for name in &vars_to_delete {
760            println!("  - {name}");
761        }
762        print!("Continue? [y/N]: ");
763        std::io::stdout().flush()?;
764
765        let mut input = String::new();
766        std::io::stdin().read_line(&mut input)?;
767
768        if !input.trim().eq_ignore_ascii_case("y") {
769            println!("Cancelled.");
770            return Ok(());
771        }
772    }
773
774    // Now we can safely delete since we're not holding any references to manager
775    for name in vars_to_delete {
776        manager.delete(&name)?;
777        println!("Deleted: {name}");
778    }
779    Ok(())
780}
781
782fn handle_analyze_command(analysis_type: &str) -> Result<()> {
783    let mut manager = EnvVarManager::new();
784    manager.load_all()?;
785    let vars = manager.list().into_iter().cloned().collect();
786    let analyzer = Analyzer::new(vars);
787
788    match analysis_type {
789        "duplicates" | "all" => {
790            let duplicates = analyzer.find_duplicates();
791            if !duplicates.is_empty() {
792                println!("Duplicate variables found:");
793                for (name, vars) in duplicates {
794                    println!("  {}: {} instances", name, vars.len());
795                }
796            }
797        }
798        "invalid" => {
799            let validation = analyzer.validate_all();
800            for (name, result) in validation {
801                if !result.valid {
802                    println!("Invalid variable: {name}");
803                    for error in result.errors {
804                        println!("  Error: {error}");
805                    }
806                }
807            }
808        }
809        _ => {}
810    }
811    Ok(())
812}
813
814#[allow(clippy::too_many_lines)]
815fn handle_path_command(action: Option<PathAction>, check: bool, var: &str, permanent: bool) -> Result<()> {
816    let mut manager = EnvVarManager::new();
817    manager.load_all()?;
818
819    // Get the PATH variable
820    let path_var = manager.get(var).ok_or_else(|| eyre!("Variable '{}' not found", var))?;
821
822    let mut path_mgr = PathManager::new(&path_var.value);
823
824    // If no action specified, list PATH entries
825    if action.is_none() {
826        if check {
827            handle_path_check(&path_mgr, true);
828        }
829        handle_path_list(&path_mgr, false, false);
830    }
831
832    let command = action.expect("Action should be Some if we reach here");
833    match command {
834        PathAction::Add {
835            directory,
836            first,
837            create,
838        } => {
839            let path = Path::new(&directory);
840
841            // Check if directory exists
842            if !path.exists() {
843                if create {
844                    std::fs::create_dir_all(path)?;
845                    println!("Created directory: {directory}");
846                } else if !path.exists() {
847                    eprintln!("Warning: Directory does not exist: {directory}");
848                    print!("Add anyway? [y/N]: ");
849                    std::io::stdout().flush()?;
850
851                    let mut input = String::new();
852                    std::io::stdin().read_line(&mut input)?;
853
854                    if !input.trim().eq_ignore_ascii_case("y") {
855                        return Ok(());
856                    }
857                }
858            }
859
860            // Check if already in PATH
861            if path_mgr.contains(&directory) {
862                println!("Directory already in {var}: {directory}");
863                return Ok(());
864            }
865
866            // Add to PATH
867            if first {
868                path_mgr.add_first(directory.clone());
869                println!("Added to beginning of {var}: {directory}");
870            } else {
871                path_mgr.add_last(directory.clone());
872                println!("Added to end of {var}: {directory}");
873            }
874
875            // Save changes
876            let new_value = path_mgr.to_string();
877            manager.set(var, &new_value, permanent)?;
878        }
879
880        PathAction::Remove { directory, all } => {
881            let removed = if all {
882                path_mgr.remove_all(&directory)
883            } else {
884                path_mgr.remove_first(&directory)
885            };
886
887            if removed > 0 {
888                println!("Removed {removed} occurrence(s) of: {directory}");
889                let new_value = path_mgr.to_string();
890                manager.set(var, &new_value, permanent)?;
891            } else {
892                println!("Directory not found in {var}: {directory}");
893            }
894        }
895
896        PathAction::Clean { dedupe, dry_run } => {
897            let invalid = path_mgr.get_invalid();
898            let duplicates = if dedupe { path_mgr.get_duplicates() } else { vec![] };
899
900            if invalid.is_empty() && duplicates.is_empty() {
901                println!("No invalid or duplicate entries found in {var}");
902                return Ok(());
903            }
904
905            if !invalid.is_empty() {
906                println!("Invalid/non-existent paths to remove:");
907                for path in &invalid {
908                    println!("  - {path}");
909                }
910            }
911
912            if !duplicates.is_empty() {
913                println!("Duplicate paths to remove:");
914                for path in &duplicates {
915                    println!("  - {path}");
916                }
917            }
918
919            if dry_run {
920                println!("\n(Dry run - no changes made)");
921            } else {
922                let removed_invalid = path_mgr.remove_invalid();
923                let removed_dupes = if dedupe {
924                    path_mgr.deduplicate(false) // Keep last by default
925                } else {
926                    0
927                };
928
929                println!("Removed {removed_invalid} invalid and {removed_dupes} duplicate entries");
930                let new_value = path_mgr.to_string();
931                manager.set(var, &new_value, permanent)?;
932            }
933        }
934
935        PathAction::Dedupe { keep_first, dry_run } => {
936            let duplicates = path_mgr.get_duplicates();
937
938            if duplicates.is_empty() {
939                println!("No duplicate entries found in {var}");
940                return Ok(());
941            }
942
943            println!("Duplicate paths to remove:");
944            for path in &duplicates {
945                println!("  - {path}");
946            }
947            println!(
948                "Strategy: keep {} occurrence",
949                if keep_first { "first" } else { "last" }
950            );
951
952            if dry_run {
953                println!("\n(Dry run - no changes made)");
954            } else {
955                let removed = path_mgr.deduplicate(keep_first);
956                println!("Removed {removed} duplicate entries");
957                let new_value = path_mgr.to_string();
958                manager.set(var, &new_value, permanent)?;
959            }
960        }
961
962        PathAction::Check { verbose } => {
963            handle_path_check(&path_mgr, verbose);
964        }
965
966        PathAction::List { numbered, check } => {
967            handle_path_list(&path_mgr, numbered, check);
968        }
969
970        PathAction::Move { from, to } => {
971            // Parse from (can be index or path)
972            let from_idx = if let Ok(idx) = from.parse::<usize>() {
973                idx
974            } else {
975                path_mgr
976                    .find_index(&from)
977                    .ok_or_else(|| eyre!("Path not found: {}", from))?
978            };
979
980            // Parse to (can be "first", "last", or index)
981            let to_idx = match to.as_str() {
982                "first" => 0,
983                "last" => path_mgr.len() - 1,
984                _ => to.parse::<usize>().map_err(|_| eyre!("Invalid position: {}", to))?,
985            };
986
987            path_mgr.move_entry(from_idx, to_idx)?;
988            println!("Moved entry from position {from_idx} to {to_idx}");
989
990            let new_value = path_mgr.to_string();
991            manager.set(var, &new_value, permanent)?;
992        }
993    }
994
995    Ok(())
996}
997
998fn handle_path_check(path_mgr: &PathManager, verbose: bool) {
999    let entries = path_mgr.entries();
1000    let mut issues = Vec::new();
1001    let mut valid_count = 0;
1002
1003    for (idx, entry) in entries.iter().enumerate() {
1004        let path = Path::new(entry);
1005        let exists = path.exists();
1006        let is_dir = path.is_dir();
1007
1008        if verbose || !exists {
1009            let status = if !exists {
1010                issues.push(format!("Not found: {entry}"));
1011                "āŒ NOT FOUND"
1012            } else if !is_dir {
1013                issues.push(format!("Not a directory: {entry}"));
1014                "āš ļø  NOT DIR"
1015            } else {
1016                valid_count += 1;
1017                "āœ“ OK"
1018            };
1019
1020            if verbose {
1021                println!("[{idx:3}] {status} - {entry}");
1022            }
1023        } else if exists && is_dir {
1024            valid_count += 1;
1025        }
1026    }
1027
1028    // Summary
1029    println!("\nPATH Analysis:");
1030    println!("  Total entries: {}", entries.len());
1031    println!("  Valid entries: {valid_count}");
1032
1033    let duplicates = path_mgr.get_duplicates();
1034    if !duplicates.is_empty() {
1035        println!("  Duplicates: {} entries", duplicates.len());
1036        if verbose {
1037            for dup in &duplicates {
1038                println!("    - {dup}");
1039            }
1040        }
1041    }
1042
1043    let invalid = path_mgr.get_invalid();
1044    if !invalid.is_empty() {
1045        println!("  Invalid entries: {}", invalid.len());
1046        if verbose {
1047            for inv in &invalid {
1048                println!("    - {inv}");
1049            }
1050        }
1051    }
1052
1053    if issues.is_empty() {
1054        println!("\nāœ… No issues found!");
1055    } else {
1056        println!("\nāš ļø  {} issue(s) found", issues.len());
1057        if !verbose {
1058            println!("Run with --verbose for details");
1059        }
1060    }
1061}
1062
1063fn handle_path_list(path_mgr: &PathManager, numbered: bool, check: bool) {
1064    let entries = path_mgr.entries();
1065
1066    if entries.is_empty() {
1067        println!("PATH is empty");
1068    }
1069
1070    for (idx, entry) in entries.iter().enumerate() {
1071        let prefix = if numbered { format!("[{idx:3}] ") } else { String::new() };
1072
1073        let suffix = if check {
1074            let path = Path::new(entry);
1075            if !path.exists() {
1076                " [NOT FOUND]"
1077            } else if !path.is_dir() {
1078                " [NOT A DIRECTORY]"
1079            } else {
1080                ""
1081            }
1082        } else {
1083            ""
1084        };
1085
1086        println!("{prefix}{entry}{suffix}");
1087    }
1088}
1089
1090fn handle_export(
1091    file: &str,
1092    vars: &[String],
1093    format: Option<String>,
1094    source: Option<String>,
1095    metadata: bool,
1096    force: bool,
1097) -> Result<()> {
1098    // Check if file exists
1099    if Path::new(&file).exists() && !force {
1100        print!("File '{file}' already exists. Overwrite? [y/N]: ");
1101        std::io::stdout().flush()?;
1102
1103        let mut input = String::new();
1104        std::io::stdin().read_line(&mut input)?;
1105
1106        if !input.trim().eq_ignore_ascii_case("y") {
1107            println!("Export cancelled.");
1108            return Ok(());
1109        }
1110    }
1111
1112    // Load environment variables
1113    let mut manager = EnvVarManager::new();
1114    manager.load_all()?;
1115
1116    // Filter variables to export
1117    let mut vars_to_export = if vars.is_empty() {
1118        manager.list().into_iter().cloned().collect()
1119    } else {
1120        let mut selected = Vec::new();
1121        for pattern in vars {
1122            let matched = manager.get_pattern(pattern);
1123            selected.extend(matched.into_iter().cloned());
1124        }
1125        selected
1126    };
1127
1128    // Filter by source if specified
1129    if let Some(src) = source {
1130        let source_filter = match src.as_str() {
1131            "system" => envx_core::EnvVarSource::System,
1132            "user" => envx_core::EnvVarSource::User,
1133            "process" => envx_core::EnvVarSource::Process,
1134            "shell" => envx_core::EnvVarSource::Shell,
1135            _ => return Err(eyre!("Invalid source: {}", src)),
1136        };
1137
1138        vars_to_export.retain(|v| v.source == source_filter);
1139    }
1140
1141    if vars_to_export.is_empty() {
1142        println!("No variables to export.");
1143        return Ok(());
1144    }
1145
1146    // Determine format
1147    let export_format = if let Some(fmt) = format {
1148        match fmt.as_str() {
1149            "env" => ExportFormat::DotEnv,
1150            "json" => ExportFormat::Json,
1151            "yaml" | "yml" => ExportFormat::Yaml,
1152            "txt" | "text" => ExportFormat::Text,
1153            "ps1" | "powershell" => ExportFormat::PowerShell,
1154            "sh" | "bash" => ExportFormat::Shell,
1155            _ => return Err(eyre!("Unsupported format: {}", fmt)),
1156        }
1157    } else {
1158        // Auto-detect from extension
1159        ExportFormat::from_extension(file)?
1160    };
1161
1162    // Export
1163    let exporter = Exporter::new(vars_to_export, metadata);
1164    exporter.export_to_file(file, export_format)?;
1165
1166    println!("Exported {} variables to '{}'", exporter.count(), file);
1167
1168    Ok(())
1169}
1170
1171fn handle_import(
1172    file: &str,
1173    vars: &[String],
1174    format: Option<String>,
1175    permanent: bool,
1176    prefix: Option<&String>,
1177    overwrite: bool,
1178    dry_run: bool,
1179) -> Result<()> {
1180    // Check if file exists
1181    if !Path::new(&file).exists() {
1182        return Err(eyre!("File not found: {}", file));
1183    }
1184
1185    // Determine format
1186    let import_format = if let Some(fmt) = format {
1187        match fmt.as_str() {
1188            "env" => ImportFormat::DotEnv,
1189            "json" => ImportFormat::Json,
1190            "yaml" | "yml" => ImportFormat::Yaml,
1191            "txt" | "text" => ImportFormat::Text,
1192            _ => return Err(eyre!("Unsupported format: {}", fmt)),
1193        }
1194    } else {
1195        // Auto-detect from extension
1196        ImportFormat::from_extension(file)?
1197    };
1198
1199    // Import variables
1200    let mut importer = Importer::new();
1201    importer.import_from_file(file, import_format)?;
1202
1203    // Filter variables if patterns specified
1204    if !vars.is_empty() {
1205        importer.filter_by_patterns(vars);
1206    }
1207
1208    // Add prefix if specified
1209    if let Some(pfx) = &prefix {
1210        importer.add_prefix(pfx);
1211    }
1212
1213    // Get variables to import
1214    let import_vars = importer.get_variables();
1215
1216    if import_vars.is_empty() {
1217        println!("No variables to import.");
1218        return Ok(());
1219    }
1220
1221    // Check for conflicts
1222    let mut manager = EnvVarManager::new();
1223    manager.load_all()?;
1224
1225    let mut conflicts = Vec::new();
1226    for (name, _) in &import_vars {
1227        if manager.get(name).is_some() {
1228            conflicts.push(name.clone());
1229        }
1230    }
1231
1232    if !conflicts.is_empty() && !overwrite && !dry_run {
1233        println!("The following variables already exist:");
1234        for name in &conflicts {
1235            println!("  - {name}");
1236        }
1237
1238        print!("Overwrite existing variables? [y/N]: ");
1239        std::io::stdout().flush()?;
1240
1241        let mut input = String::new();
1242        std::io::stdin().read_line(&mut input)?;
1243
1244        if !input.trim().eq_ignore_ascii_case("y") {
1245            println!("Import cancelled.");
1246            return Ok(());
1247        }
1248    }
1249
1250    // Preview or apply changes
1251    if dry_run {
1252        println!("Would import {} variables:", import_vars.len());
1253        for (name, value) in &import_vars {
1254            let status = if conflicts.contains(name) {
1255                " [OVERWRITE]"
1256            } else {
1257                " [NEW]"
1258            };
1259            println!(
1260                "  {} = {}{}",
1261                name,
1262                if value.len() > 50 {
1263                    format!("{}...", &value[..50])
1264                } else {
1265                    value.clone()
1266                },
1267                status
1268            );
1269        }
1270        println!("\n(Dry run - no changes made)");
1271    } else {
1272        // Apply imports
1273        let mut imported = 0;
1274        let mut failed = 0;
1275
1276        for (name, value) in import_vars {
1277            match manager.set(&name, &value, permanent) {
1278                Ok(()) => imported += 1,
1279                Err(e) => {
1280                    eprintln!("Failed to import {name}: {e}");
1281                    failed += 1;
1282                }
1283            }
1284        }
1285
1286        println!("Imported {imported} variables");
1287        if failed > 0 {
1288            println!("Failed to import {failed} variables");
1289        }
1290    }
1291
1292    Ok(())
1293}
1294
1295fn handle_list_command(
1296    source: Option<&str>,
1297    query: Option<&str>,
1298    format: &str,
1299    sort: &str,
1300    names_only: bool,
1301    limit: Option<usize>,
1302    stats: bool,
1303) -> Result<()> {
1304    let mut manager = EnvVarManager::new();
1305    manager.load_all()?;
1306
1307    // Get filtered variables
1308    let mut vars = if let Some(q) = &query {
1309        manager.search(q)
1310    } else if let Some(src) = source {
1311        let source_filter = match src {
1312            "system" => envx_core::EnvVarSource::System,
1313            "user" => envx_core::EnvVarSource::User,
1314            "process" => envx_core::EnvVarSource::Process,
1315            "shell" => envx_core::EnvVarSource::Shell,
1316            _ => return Err(eyre!("Invalid source: {}", src)),
1317        };
1318        manager.filter_by_source(&source_filter)
1319    } else {
1320        manager.list()
1321    };
1322
1323    // Sort variables
1324    match sort {
1325        "name" => vars.sort_by(|a, b| a.name.cmp(&b.name)),
1326        "value" => vars.sort_by(|a, b| a.value.cmp(&b.value)),
1327        "source" => vars.sort_by(|a, b| format!("{:?}", a.source).cmp(&format!("{:?}", b.source))),
1328        _ => {}
1329    }
1330
1331    // Apply limit if specified
1332    let total_count = vars.len();
1333    if let Some(lim) = limit {
1334        vars.truncate(lim);
1335    }
1336
1337    // Show statistics if requested
1338    if stats || (format == "table" && !names_only) {
1339        print_statistics(&manager, &vars, total_count, query, source);
1340    }
1341
1342    // Handle names_only flag
1343    if names_only {
1344        for var in vars {
1345            println!("{}", var.name);
1346        }
1347        return Ok(());
1348    }
1349
1350    // Format output
1351    match format {
1352        "json" => {
1353            println!("{}", serde_json::to_string_pretty(&vars)?);
1354        }
1355        "simple" => {
1356            for var in vars {
1357                println!("{} = {}", style(&var.name).cyan(), var.value);
1358            }
1359        }
1360        "compact" => {
1361            for var in vars {
1362                let source_str = format_source_compact(&var.source);
1363                println!(
1364                    "{} {} = {}",
1365                    source_str,
1366                    style(&var.name).bright(),
1367                    style(truncate_value(&var.value, 60)).dim()
1368                );
1369            }
1370        }
1371        _ => {
1372            print_table(vars, limit.is_some());
1373        }
1374    }
1375
1376    // Show limit notice
1377    if let Some(lim) = limit {
1378        if total_count > lim {
1379            println!(
1380                "\n{}",
1381                style(format!(
1382                    "Showing {lim} of {total_count} total variables. Use --limit to see more."
1383                ))
1384                .yellow()
1385            );
1386        }
1387    }
1388
1389    Ok(())
1390}
1391
1392/// Handle snapshot-related commands.
1393///
1394/// # Errors
1395///
1396/// This function will return an error if:
1397/// - The snapshot manager cannot be initialized
1398/// - Environment variable loading fails
1399/// - Snapshot operations fail (create, restore, delete, etc.)
1400/// - File I/O operations fail during snapshot operations
1401/// - User input cannot be read from stdin
1402/// - Invalid snapshot names or IDs are provided
1403pub fn handle_snapshot(args: SnapshotArgs) -> Result<()> {
1404    let snapshot_manager = SnapshotManager::new()?;
1405    let mut env_manager = EnvVarManager::new();
1406    env_manager.load_all()?;
1407
1408    match args.command {
1409        SnapshotCommands::Create { name, description } => {
1410            let vars = env_manager.list().into_iter().cloned().collect();
1411            let snapshot = snapshot_manager.create(name, description, vars)?;
1412            println!("āœ… Created snapshot: {} (ID: {})", snapshot.name, snapshot.id);
1413        }
1414        SnapshotCommands::List => {
1415            let snapshots = snapshot_manager.list()?;
1416            if snapshots.is_empty() {
1417                println!("No snapshots found.");
1418                return Ok(());
1419            }
1420
1421            let mut table = Table::new();
1422            table.set_header(vec!["Name", "ID", "Created", "Variables", "Description"]);
1423
1424            for snapshot in snapshots {
1425                table.add_row(vec![
1426                    snapshot.name,
1427                    snapshot.id[..8].to_string(),
1428                    snapshot.created_at.format("%Y-%m-%d %H:%M").to_string(),
1429                    snapshot.variables.len().to_string(),
1430                    snapshot.description.unwrap_or_default(),
1431                ]);
1432            }
1433
1434            println!("{table}");
1435        }
1436        SnapshotCommands::Show { snapshot } => {
1437            let snap = snapshot_manager.get(&snapshot)?;
1438            println!("Snapshot: {}", snap.name);
1439            println!("ID: {}", snap.id);
1440            println!("Created: {}", snap.created_at.format("%Y-%m-%d %H:%M:%S"));
1441            println!("Description: {}", snap.description.unwrap_or_default());
1442            println!("Variables: {}", snap.variables.len());
1443
1444            // Show first 10 variables
1445            println!("\nFirst 10 variables:");
1446            for (i, (name, var)) in snap.variables.iter().take(10).enumerate() {
1447                println!("  {}. {} = {}", i + 1, name, var.value);
1448            }
1449
1450            if snap.variables.len() > 10 {
1451                println!("  ... and {} more", snap.variables.len() - 10);
1452            }
1453        }
1454        SnapshotCommands::Restore { snapshot, force } => {
1455            if !force {
1456                print!("āš ļø  This will replace all current environment variables. Continue? [y/N] ");
1457                std::io::Write::flush(&mut std::io::stdout())?;
1458
1459                let mut input = String::new();
1460                std::io::stdin().read_line(&mut input)?;
1461                if !input.trim().eq_ignore_ascii_case("y") {
1462                    println!("Cancelled.");
1463                    return Ok(());
1464                }
1465            }
1466
1467            snapshot_manager.restore(&snapshot, &mut env_manager)?;
1468            println!("āœ… Restored from snapshot: {snapshot}");
1469        }
1470        SnapshotCommands::Delete { snapshot, force } => {
1471            if !force {
1472                print!("āš ļø  Delete snapshot '{snapshot}'? [y/N] ");
1473                std::io::Write::flush(&mut std::io::stdout())?;
1474
1475                let mut input = String::new();
1476                std::io::stdin().read_line(&mut input)?;
1477                if !input.trim().eq_ignore_ascii_case("y") {
1478                    println!("Cancelled.");
1479                    return Ok(());
1480                }
1481            }
1482
1483            snapshot_manager.delete(&snapshot)?;
1484            println!("āœ… Deleted snapshot: {snapshot}");
1485        }
1486        SnapshotCommands::Diff { snapshot1, snapshot2 } => {
1487            let diff = snapshot_manager.diff(&snapshot1, &snapshot2)?;
1488
1489            if diff.added.is_empty() && diff.removed.is_empty() && diff.modified.is_empty() {
1490                println!("No differences found between snapshots.");
1491                return Ok(());
1492            }
1493
1494            if !diff.added.is_empty() {
1495                println!("āž• Added in {snapshot2}:");
1496                for (name, var) in &diff.added {
1497                    println!("   {} = {}", name, var.value);
1498                }
1499            }
1500
1501            if !diff.removed.is_empty() {
1502                println!("\nāž– Removed in {snapshot2}:");
1503                for (name, var) in &diff.removed {
1504                    println!("   {} = {}", name, var.value);
1505                }
1506            }
1507
1508            if !diff.modified.is_empty() {
1509                println!("\nšŸ”„ Modified:");
1510                for (name, (old, new)) in &diff.modified {
1511                    println!("   {name}:");
1512                    println!("     Old: {}", old.value);
1513                    println!("     New: {}", new.value);
1514                }
1515            }
1516        }
1517    }
1518
1519    Ok(())
1520}
1521
1522/// Handle profile-related commands.
1523///
1524/// # Errors
1525///
1526/// This function will return an error if:
1527/// - The profile manager cannot be initialized
1528/// - Environment variable loading fails  
1529/// - Profile operations fail (create, switch, delete, etc.)
1530/// - File I/O operations fail during profile import/export
1531/// - User input cannot be read from stdin
1532/// - Invalid profile names are provided
1533/// - Profile data cannot be serialized/deserialized
1534pub fn handle_profile(args: ProfileArgs) -> Result<()> {
1535    let mut profile_manager = ProfileManager::new()?;
1536    let mut env_manager = EnvVarManager::new();
1537    env_manager.load_all()?;
1538
1539    match args.command {
1540        ProfileCommands::Create { name, description } => {
1541            handle_profile_create(&mut profile_manager, &name, description)?;
1542        }
1543        ProfileCommands::List => {
1544            handle_profile_list(&profile_manager);
1545        }
1546        ProfileCommands::Show { name } => {
1547            handle_profile_show(&profile_manager, name)?;
1548        }
1549        ProfileCommands::Switch { name, apply } => {
1550            handle_profile_switch(&mut profile_manager, &mut env_manager, &name, apply)?;
1551        }
1552        ProfileCommands::Add {
1553            profile,
1554            name,
1555            value,
1556            override_system,
1557        } => {
1558            handle_profile_add(&mut profile_manager, &profile, &name, &value, override_system)?;
1559        }
1560        ProfileCommands::Remove { profile, name } => {
1561            handle_profile_remove(&mut profile_manager, &profile, &name)?;
1562        }
1563        ProfileCommands::Delete { name, force } => {
1564            handle_profile_delete(&mut profile_manager, &name, force)?;
1565        }
1566        ProfileCommands::Export { name, output } => {
1567            handle_profile_export(&profile_manager, &name, output)?;
1568        }
1569        ProfileCommands::Import { file, name, overwrite } => {
1570            handle_profile_import(&mut profile_manager, &file, name, overwrite)?;
1571        }
1572        ProfileCommands::Apply { name } => {
1573            handle_profile_apply(&mut profile_manager, &mut env_manager, &name)?;
1574        }
1575    }
1576
1577    Ok(())
1578}
1579
1580fn handle_profile_create(profile_manager: &mut ProfileManager, name: &str, description: Option<String>) -> Result<()> {
1581    profile_manager.create(name.to_string(), description)?;
1582    println!("āœ… Created profile: {name}");
1583    Ok(())
1584}
1585
1586fn handle_profile_list(profile_manager: &ProfileManager) {
1587    let profiles = profile_manager.list();
1588    if profiles.is_empty() {
1589        println!("No profiles found.");
1590    }
1591
1592    let active = profile_manager.active().map(|p| &p.name);
1593    let mut table = Table::new();
1594    table.set_header(vec!["Name", "Variables", "Created", "Description", "Status"]);
1595
1596    for profile in profiles {
1597        let status = if active == Some(&profile.name) {
1598            "ā— Active"
1599        } else {
1600            ""
1601        };
1602
1603        table.add_row(vec![
1604            profile.name.clone(),
1605            profile.variables.len().to_string(),
1606            profile.created_at.format("%Y-%m-%d").to_string(),
1607            profile.description.clone().unwrap_or_default(),
1608            status.to_string(),
1609        ]);
1610    }
1611
1612    println!("{table}");
1613}
1614
1615fn handle_profile_show(profile_manager: &ProfileManager, name: Option<String>) -> Result<()> {
1616    let profile = if let Some(name) = name {
1617        profile_manager
1618            .get(&name)
1619            .ok_or_else(|| color_eyre::eyre::eyre!("Profile '{}' not found", name))?
1620    } else {
1621        profile_manager
1622            .active()
1623            .ok_or_else(|| color_eyre::eyre::eyre!("No active profile"))?
1624    };
1625
1626    println!("Profile: {}", profile.name);
1627    println!("Description: {}", profile.description.as_deref().unwrap_or(""));
1628    println!("Created: {}", profile.created_at.format("%Y-%m-%d %H:%M:%S"));
1629    println!("Updated: {}", profile.updated_at.format("%Y-%m-%d %H:%M:%S"));
1630    if let Some(parent) = &profile.parent {
1631        println!("Inherits from: {parent}");
1632    }
1633    println!("\nVariables:");
1634
1635    for (name, var) in &profile.variables {
1636        let status = if var.enabled { "āœ“" } else { "āœ—" };
1637        let override_flag = if var.override_system { " [override]" } else { "" };
1638        println!("  {} {} = {}{}", status, name, var.value, override_flag);
1639    }
1640    Ok(())
1641}
1642
1643fn handle_profile_switch(
1644    profile_manager: &mut ProfileManager,
1645    env_manager: &mut EnvVarManager,
1646    name: &str,
1647    apply: bool,
1648) -> Result<()> {
1649    profile_manager.switch(name)?;
1650    println!("āœ… Switched to profile: {name}");
1651
1652    if apply {
1653        profile_manager.apply(name, env_manager)?;
1654        println!("āœ… Applied profile variables");
1655    }
1656    Ok(())
1657}
1658
1659fn handle_profile_add(
1660    profile_manager: &mut ProfileManager,
1661    profile: &str,
1662    name: &str,
1663    value: &str,
1664    override_system: bool,
1665) -> Result<()> {
1666    let prof = profile_manager
1667        .get_mut(profile)
1668        .ok_or_else(|| color_eyre::eyre::eyre!("Profile '{}' not found", profile))?;
1669
1670    prof.add_var(name.to_string(), value.to_string(), override_system);
1671    profile_manager.save()?;
1672
1673    println!("āœ… Added {name} to profile {profile}");
1674    Ok(())
1675}
1676
1677fn handle_profile_remove(profile_manager: &mut ProfileManager, profile: &str, name: &str) -> Result<()> {
1678    let prof = profile_manager
1679        .get_mut(profile)
1680        .ok_or_else(|| color_eyre::eyre::eyre!("Profile '{}' not found", profile))?;
1681
1682    prof.remove_var(name)
1683        .ok_or_else(|| color_eyre::eyre::eyre!("Variable '{}' not found in profile", name))?;
1684
1685    profile_manager.save()?;
1686    println!("āœ… Removed {name} from profile {profile}");
1687    Ok(())
1688}
1689
1690fn handle_profile_delete(profile_manager: &mut ProfileManager, name: &str, force: bool) -> Result<()> {
1691    if !force {
1692        print!("āš ļø  Delete profile '{name}'? [y/N] ");
1693        std::io::Write::flush(&mut std::io::stdout())?;
1694
1695        let mut input = String::new();
1696        std::io::stdin().read_line(&mut input)?;
1697        if !input.trim().eq_ignore_ascii_case("y") {
1698            println!("Cancelled.");
1699            return Ok(());
1700        }
1701    }
1702
1703    profile_manager.delete(name)?;
1704    println!("āœ… Deleted profile: {name}");
1705    Ok(())
1706}
1707
1708fn handle_profile_export(profile_manager: &ProfileManager, name: &str, output: Option<String>) -> Result<()> {
1709    let json = profile_manager.export(name)?;
1710
1711    if let Some(path) = output {
1712        std::fs::write(path, json)?;
1713        println!("āœ… Exported profile to file");
1714    } else {
1715        println!("{json}");
1716    }
1717    Ok(())
1718}
1719
1720fn handle_profile_import(
1721    profile_manager: &mut ProfileManager,
1722    file: &str,
1723    name: Option<String>,
1724    overwrite: bool,
1725) -> Result<()> {
1726    let content = std::fs::read_to_string(file)?;
1727    let import_name = name.unwrap_or_else(|| "imported".to_string());
1728
1729    profile_manager.import(import_name.clone(), &content, overwrite)?;
1730    println!("āœ… Imported profile: {import_name}");
1731    Ok(())
1732}
1733
1734fn handle_profile_apply(
1735    profile_manager: &mut ProfileManager,
1736    env_manager: &mut EnvVarManager,
1737    name: &str,
1738) -> Result<()> {
1739    profile_manager.apply(name, env_manager)?;
1740    println!("āœ… Applied profile: {name}");
1741    Ok(())
1742}
1743
1744fn print_statistics(
1745    manager: &EnvVarManager,
1746    filtered_vars: &[&envx_core::EnvVar],
1747    total_count: usize,
1748    query: Option<&str>,
1749    source: Option<&str>,
1750) {
1751    let _term = Term::stdout();
1752
1753    // Count by source
1754    let system_count = manager.filter_by_source(&envx_core::EnvVarSource::System).len();
1755    let user_count = manager.filter_by_source(&envx_core::EnvVarSource::User).len();
1756    let process_count = manager.filter_by_source(&envx_core::EnvVarSource::Process).len();
1757    let shell_count = manager.filter_by_source(&envx_core::EnvVarSource::Shell).len();
1758
1759    // Header
1760    println!("{}", style("═".repeat(60)).blue().bold());
1761    println!("{}", style("Environment Variables Summary").cyan().bold());
1762    println!("{}", style("═".repeat(60)).blue().bold());
1763
1764    // Filter info
1765    if query.is_some() || source.is_some() {
1766        print!("  {} ", style("Filter:").yellow());
1767        if let Some(q) = query {
1768            print!("query='{}' ", style(q).green());
1769        }
1770        if let Some(s) = source {
1771            print!("source={} ", style(s).green());
1772        }
1773        println!();
1774        println!(
1775            "  {} {}/{} variables",
1776            style("Showing:").yellow(),
1777            style(filtered_vars.len()).green().bold(),
1778            total_count
1779        );
1780    } else {
1781        println!(
1782            "  {} {} variables",
1783            style("Total:").yellow(),
1784            style(total_count).green().bold()
1785        );
1786    }
1787
1788    println!();
1789    println!("  {} By Source:", style("ā–ŗ").cyan());
1790
1791    // Source breakdown with visual bars
1792    let max_count = system_count.max(user_count).max(process_count).max(shell_count);
1793    let bar_width = 30;
1794
1795    print_source_bar("System", system_count, max_count, bar_width, "red");
1796    print_source_bar("User", user_count, max_count, bar_width, "yellow");
1797    print_source_bar("Process", process_count, max_count, bar_width, "green");
1798    print_source_bar("Shell", shell_count, max_count, bar_width, "cyan");
1799
1800    println!("{}", style("─".repeat(60)).blue());
1801    println!();
1802}
1803
1804fn print_source_bar(label: &str, count: usize, max: usize, width: usize, color: &str) {
1805    let filled = if max > 0 { (count * width / max).max(1) } else { 0 };
1806
1807    let bar = "ā–ˆ".repeat(filled);
1808    let empty = "ā–‘".repeat(width - filled);
1809
1810    let colored_bar = match color {
1811        "red" => style(bar).red(),
1812        "yellow" => style(bar).yellow(),
1813        "green" => style(bar).green(),
1814        "cyan" => style(bar).cyan(),
1815        _ => style(bar).white(),
1816    };
1817
1818    println!(
1819        "    {:10} {} {}{} {}",
1820        style(label).bold(),
1821        colored_bar,
1822        style(empty).dim(),
1823        style(format!(" {count:4}")).bold(),
1824        style("vars").dim()
1825    );
1826}
1827
1828fn print_table(vars: Vec<&envx_core::EnvVar>, _is_limited: bool) {
1829    if vars.is_empty() {
1830        println!("{}", style("No environment variables found.").yellow());
1831    }
1832
1833    let mut table = Table::new();
1834
1835    // Configure table style
1836    table
1837        .set_content_arrangement(ContentArrangement::Dynamic)
1838        .set_width(120)
1839        .set_header(vec![
1840            Cell::new("Source").add_attribute(Attribute::Bold).fg(Color::Cyan),
1841            Cell::new("Name").add_attribute(Attribute::Bold).fg(Color::Cyan),
1842            Cell::new("Value").add_attribute(Attribute::Bold).fg(Color::Cyan),
1843        ]);
1844
1845    // Add rows with colored source indicators
1846    for var in vars {
1847        let (source_str, source_color) = format_source(&var.source);
1848        let truncated_value = truncate_value(&var.value, 50);
1849
1850        table.add_row(vec![
1851            Cell::new(source_str).fg(source_color),
1852            Cell::new(&var.name).fg(Color::White),
1853            Cell::new(truncated_value).fg(Color::Grey),
1854        ]);
1855    }
1856
1857    println!("{table}");
1858}
1859
1860fn format_source(source: &envx_core::EnvVarSource) -> (String, Color) {
1861    match source {
1862        envx_core::EnvVarSource::System => ("System".to_string(), Color::Red),
1863        envx_core::EnvVarSource::User => ("User".to_string(), Color::Yellow),
1864        envx_core::EnvVarSource::Process => ("Process".to_string(), Color::Green),
1865        envx_core::EnvVarSource::Shell => ("Shell".to_string(), Color::Cyan),
1866        envx_core::EnvVarSource::Application(app) => (format!("App:{app}"), Color::Magenta),
1867    }
1868}
1869
1870fn format_source_compact(source: &envx_core::EnvVarSource) -> console::StyledObject<String> {
1871    match source {
1872        envx_core::EnvVarSource::System => style("[SYS]".to_string()).red().bold(),
1873        envx_core::EnvVarSource::User => style("[USR]".to_string()).yellow().bold(),
1874        envx_core::EnvVarSource::Process => style("[PRC]".to_string()).green().bold(),
1875        envx_core::EnvVarSource::Shell => style("[SHL]".to_string()).cyan().bold(),
1876        envx_core::EnvVarSource::Application(app) => style(format!("[{}]", &app[..3.min(app.len())].to_uppercase()))
1877            .magenta()
1878            .bold(),
1879    }
1880}
1881
1882fn truncate_value(value: &str, max_len: usize) -> String {
1883    if value.len() <= max_len {
1884        value.to_string()
1885    } else {
1886        format!("{}...", &value[..max_len - 3])
1887    }
1888}
1889
1890/// Handle project-related commands.
1891///
1892/// # Errors
1893///
1894/// This function will return an error if:
1895/// - Project manager initialization fails
1896/// - Environment variable manager operations fail
1897/// - Project configuration file cannot be found, read, or written
1898/// - Project validation fails (when not using --force)
1899/// - Profile manager operations fail
1900/// - Script execution fails
1901/// - Required variable configuration cannot be updated
1902/// - File I/O operations fail during project operations
1903#[allow(clippy::too_many_lines)]
1904pub fn handle_project(args: ProjectArgs) -> Result<()> {
1905    match args.command {
1906        ProjectCommands::Init { name } => {
1907            let manager = ProjectManager::new()?;
1908            manager.init(name)?;
1909        }
1910
1911        ProjectCommands::Apply { force } => {
1912            let mut project = ProjectManager::new()?;
1913            let mut env_manager = EnvVarManager::new();
1914            let mut profile_manager = ProfileManager::new()?;
1915
1916            if let Some(project_dir) = project.find_and_load()? {
1917                println!("šŸ“ Found project at: {}", project_dir.display());
1918
1919                // Validate first
1920                let report = project.validate(&env_manager)?;
1921
1922                if !report.success && !force {
1923                    print_validation_report(&report);
1924                    return Err(color_eyre::eyre::eyre!(
1925                        "Validation failed. Use --force to apply anyway."
1926                    ));
1927                }
1928
1929                // Apply configuration
1930                project.apply(&mut env_manager, &mut profile_manager)?;
1931                println!("āœ… Applied project configuration");
1932
1933                if !report.warnings.is_empty() {
1934                    println!("\nāš ļø  Warnings:");
1935                    for warning in &report.warnings {
1936                        println!("  - {}: {}", warning.var_name, warning.message);
1937                    }
1938                }
1939            } else {
1940                return Err(color_eyre::eyre::eyre!(
1941                    "No .envx/config.yaml found in current or parent directories"
1942                ));
1943            }
1944        }
1945
1946        ProjectCommands::Check => {
1947            let mut project = ProjectManager::new()?;
1948            let env_manager = EnvVarManager::new();
1949
1950            if project.find_and_load()?.is_some() {
1951                let report = project.validate(&env_manager)?;
1952                print_validation_report(&report);
1953
1954                if !report.success {
1955                    std::process::exit(1);
1956                }
1957            } else {
1958                return Err(color_eyre::eyre::eyre!("No project configuration found"));
1959            }
1960        }
1961
1962        ProjectCommands::Edit => {
1963            let _ = ProjectManager::new()?;
1964            let config_path = std::env::current_dir()?.join(".envx").join("config.yaml");
1965
1966            if !config_path.exists() {
1967                return Err(color_eyre::eyre::eyre!(
1968                    "No .envx/config.yaml found. Run 'envx init' first."
1969                ));
1970            }
1971
1972            #[cfg(windows)]
1973            {
1974                std::process::Command::new("notepad").arg(&config_path).spawn()?;
1975            }
1976
1977            #[cfg(unix)]
1978            {
1979                let editor = std::env::var("EDITOR").unwrap_or_else(|_| "nano".to_string());
1980                std::process::Command::new(editor).arg(&config_path).spawn()?;
1981            }
1982
1983            println!("šŸ“ Opening config in editor...");
1984        }
1985
1986        ProjectCommands::Info => {
1987            let mut project = ProjectManager::new()?;
1988
1989            if let Some(project_dir) = project.find_and_load()? {
1990                // Load and display config
1991                let config_path = project_dir.join(".envx").join("config.yaml");
1992                let content = std::fs::read_to_string(&config_path)?;
1993
1994                println!("šŸ“ Project Directory: {}", project_dir.display());
1995                println!("\nšŸ“„ Configuration:");
1996                println!("{content}");
1997            } else {
1998                return Err(color_eyre::eyre::eyre!("No project configuration found"));
1999            }
2000        }
2001
2002        ProjectCommands::Run { script } => {
2003            let mut project = ProjectManager::new()?;
2004            let mut env_manager = EnvVarManager::new();
2005
2006            if project.find_and_load()?.is_some() {
2007                project.run_script(&script, &mut env_manager)?;
2008                println!("āœ… Script '{script}' completed");
2009            } else {
2010                return Err(color_eyre::eyre::eyre!("No project configuration found"));
2011            }
2012        }
2013
2014        ProjectCommands::Require {
2015            name,
2016            description,
2017            pattern,
2018            example,
2019        } => {
2020            let config_path = std::env::current_dir()?.join(".envx").join("config.yaml");
2021
2022            if !config_path.exists() {
2023                return Err(color_eyre::eyre::eyre!(
2024                    "No .envx/config.yaml found. Run 'envx init' first."
2025                ));
2026            }
2027
2028            // Load, modify, and save config
2029            let mut config = ProjectConfig::load(&config_path)?;
2030            config.required.push(RequiredVar {
2031                name: name.clone(),
2032                description,
2033                pattern,
2034                example,
2035            });
2036            config.save(&config_path)?;
2037
2038            println!("āœ… Added required variable: {name}");
2039        }
2040    }
2041
2042    Ok(())
2043}
2044
2045fn print_validation_report(report: &ValidationReport) {
2046    if report.success {
2047        println!("āœ… All required variables are set!");
2048        return;
2049    }
2050
2051    if !report.missing.is_empty() {
2052        println!("āŒ Missing required variables:");
2053        let mut table = Table::new();
2054        table.set_header(vec!["Variable", "Description", "Example"]);
2055
2056        for var in &report.missing {
2057            table.add_row(vec![
2058                var.name.clone(),
2059                var.description.clone().unwrap_or_default(),
2060                var.example.clone().unwrap_or_default(),
2061            ]);
2062        }
2063
2064        println!("{table}");
2065    }
2066
2067    if !report.errors.is_empty() {
2068        println!("\nāŒ Validation errors:");
2069        for error in &report.errors {
2070            println!("  - {}: {}", error.var_name, error.message);
2071        }
2072    }
2073}
2074
2075/// Handle rename command to rename environment variables using patterns.
2076///
2077/// # Errors
2078///
2079/// This function will return an error if:
2080/// - Environment variable operations fail (loading, renaming)
2081/// - Pattern matching fails or produces invalid results
2082/// - Variable names conflict or are invalid
2083/// - File I/O operations fail when persisting changes
2084/// - User input cannot be read from stdin during confirmation
2085pub fn handle_rename(args: &RenameArgs) -> Result<()> {
2086    let mut manager = EnvVarManager::new();
2087    manager.load_all()?;
2088
2089    if args.dry_run {
2090        // Show what would be renamed
2091        let preview = preview_rename(&manager, &args.pattern, &args.replacement)?;
2092
2093        if preview.is_empty() {
2094            println!("No variables match the pattern '{}'", args.pattern);
2095        } else {
2096            println!("Would rename {} variable(s):", preview.len());
2097
2098            let mut table = Table::new();
2099            table.load_preset(UTF8_FULL);
2100            table.set_header(vec!["Current Name", "New Name", "Value"]);
2101
2102            for (old, new, value) in preview {
2103                table.add_row(vec![old, new, value]);
2104            }
2105
2106            println!("{table}");
2107            println!("\nUse without --dry-run to apply changes");
2108        }
2109    } else {
2110        let renamed = manager.rename(&args.pattern, &args.replacement)?;
2111
2112        if renamed.is_empty() {
2113            println!("No variables match the pattern '{}'", args.pattern);
2114        } else {
2115            println!("āœ… Renamed {} variable(s):", renamed.len());
2116
2117            let mut table = Table::new();
2118            table.load_preset(UTF8_FULL);
2119            table.set_header(vec!["Old Name", "New Name"]);
2120
2121            for (old, new) in &renamed {
2122                table.add_row(vec![old.clone(), new.clone()]);
2123            }
2124
2125            println!("{table}");
2126
2127            #[cfg(windows)]
2128            println!("\nšŸ“ Note: You may need to restart your terminal for changes to take effect");
2129        }
2130    }
2131
2132    Ok(())
2133}
2134
2135fn preview_rename(manager: &EnvVarManager, pattern: &str, replacement: &str) -> Result<Vec<(String, String, String)>> {
2136    let mut preview = Vec::new();
2137
2138    if pattern.contains('*') {
2139        let (prefix, suffix) = split_wildcard_pattern(pattern)?;
2140        let (new_prefix, new_suffix) = split_wildcard_pattern(replacement)?;
2141
2142        for var in manager.list() {
2143            if var.name.starts_with(&prefix)
2144                && var.name.ends_with(&suffix)
2145                && var.name.len() >= prefix.len() + suffix.len()
2146            {
2147                let middle = &var.name[prefix.len()..var.name.len() - suffix.len()];
2148                let new_name = format!("{new_prefix}{middle}{new_suffix}");
2149                preview.push((var.name.clone(), new_name, var.value.clone()));
2150            }
2151        }
2152    } else if let Some(var) = manager.get(pattern) {
2153        preview.push((var.name.clone(), replacement.to_string(), var.value.clone()));
2154    }
2155
2156    Ok(preview)
2157}
2158
2159/// Handle replace command to replace environment variable values using patterns.
2160///
2161/// # Errors
2162///
2163/// This function will return an error if:
2164/// - Environment variable operations fail (loading, replacing)
2165/// - Pattern matching fails or produces invalid results
2166/// - File I/O operations fail when persisting changes
2167/// - Wildcard pattern parsing fails
2168pub fn handle_replace(args: &ReplaceArgs) -> Result<()> {
2169    let mut manager = EnvVarManager::new();
2170    manager.load_all()?;
2171
2172    if args.dry_run {
2173        // Show what would be replaced
2174        let preview = preview_replace(&manager, &args.pattern)?;
2175
2176        if preview.is_empty() {
2177            println!("No variables match the pattern '{}'", args.pattern);
2178        } else {
2179            println!("Would update {} variable(s):", preview.len());
2180
2181            let mut table = Table::new();
2182            table.load_preset(UTF8_FULL);
2183            table.set_header(vec!["Variable", "Current Value", "New Value"]);
2184
2185            for (name, current) in preview {
2186                table.add_row(vec![name, current, args.value.clone()]);
2187            }
2188
2189            println!("{table}");
2190            println!("\nUse without --dry-run to apply changes");
2191        }
2192    } else {
2193        let replaced = manager.replace(&args.pattern, &args.value)?;
2194
2195        if replaced.is_empty() {
2196            println!("No variables match the pattern '{}'", args.pattern);
2197        } else {
2198            println!("āœ… Updated {} variable(s):", replaced.len());
2199
2200            let mut table = Table::new();
2201            table.load_preset(UTF8_FULL);
2202            table.set_header(vec!["Variable", "Old Value", "New Value"]);
2203
2204            for (name, old, new) in &replaced {
2205                // Truncate long values for display
2206                let display_old = if old.len() > 50 {
2207                    format!("{}...", &old[..47])
2208                } else {
2209                    old.clone()
2210                };
2211                let display_new = if new.len() > 50 {
2212                    format!("{}...", &new[..47])
2213                } else {
2214                    new.clone()
2215                };
2216                table.add_row(vec![name.clone(), display_old, display_new]);
2217            }
2218
2219            println!("{table}");
2220
2221            #[cfg(windows)]
2222            println!("\nšŸ“ Note: You may need to restart your terminal for changes to take effect");
2223        }
2224    }
2225
2226    Ok(())
2227}
2228
2229fn preview_replace(manager: &EnvVarManager, pattern: &str) -> Result<Vec<(String, String)>> {
2230    let mut preview = Vec::new();
2231
2232    if pattern.contains('*') {
2233        let (prefix, suffix) = split_wildcard_pattern(pattern)?;
2234
2235        for var in manager.list() {
2236            if var.name.starts_with(&prefix)
2237                && var.name.ends_with(&suffix)
2238                && var.name.len() >= prefix.len() + suffix.len()
2239            {
2240                preview.push((var.name.clone(), var.value.clone()));
2241            }
2242        }
2243    } else if let Some(var) = manager.get(pattern) {
2244        preview.push((var.name.clone(), var.value.clone()));
2245    }
2246
2247    Ok(preview)
2248}
2249
2250/// Handle find and replace operations within environment variable values.
2251///
2252/// # Errors
2253///
2254/// This function will return an error if:
2255/// - Environment variable operations fail (loading, updating)
2256/// - Pattern matching fails or produces invalid results
2257/// - Find and replace operations fail
2258/// - File I/O operations fail when persisting changes
2259/// - Wildcard pattern parsing fails
2260pub fn handle_find_replace(args: &FindReplaceArgs) -> Result<()> {
2261    let mut manager = EnvVarManager::new();
2262    manager.load_all()?;
2263
2264    if args.dry_run {
2265        // Show preview
2266        let preview = preview_find_replace(&manager, &args.search, &args.replacement, args.pattern.as_deref())?;
2267
2268        if preview.is_empty() {
2269            println!("No variables contain '{}'", args.search);
2270        } else {
2271            println!("Would update {} variable(s):", preview.len());
2272
2273            let mut table = Table::new();
2274            table.load_preset(UTF8_FULL);
2275            table.set_header(vec!["Variable", "Current Value", "New Value"]);
2276
2277            for (name, old, new) in preview {
2278                table.add_row(vec![name, old, new]);
2279            }
2280
2281            println!("{table}");
2282            println!("\nUse without --dry-run to apply changes");
2283        }
2284    } else {
2285        let replaced = manager.find_replace(&args.search, &args.replacement, args.pattern.as_deref())?;
2286
2287        if replaced.is_empty() {
2288            println!("No variables contain '{}'", args.search);
2289        } else {
2290            println!("āœ… Updated {} variable(s):", replaced.len());
2291
2292            let mut table = Table::new();
2293            table.load_preset(UTF8_FULL);
2294            table.set_header(vec!["Variable", "Old Value", "New Value"]);
2295
2296            for (name, old, new) in &replaced {
2297                // Truncate long values
2298                let display_old = if old.len() > 50 {
2299                    format!("{}...", &old[..47])
2300                } else {
2301                    old.clone()
2302                };
2303                let display_new = if new.len() > 50 {
2304                    format!("{}...", &new[..47])
2305                } else {
2306                    new.clone()
2307                };
2308                table.add_row(vec![name.clone(), display_old, display_new]);
2309            }
2310
2311            println!("{table}");
2312
2313            #[cfg(windows)]
2314            println!("\nšŸ“ Note: You may need to restart your terminal for changes to take effect");
2315        }
2316    }
2317
2318    Ok(())
2319}
2320
2321fn preview_find_replace(
2322    manager: &EnvVarManager,
2323    search: &str,
2324    replacement: &str,
2325    pattern: Option<&str>,
2326) -> Result<Vec<(String, String, String)>> {
2327    let mut preview = Vec::new();
2328
2329    for var in manager.list() {
2330        // Check if variable matches pattern (if specified)
2331        let matches_pattern = if let Some(pat) = pattern {
2332            if pat.contains('*') {
2333                let (prefix, suffix) = split_wildcard_pattern(pat)?;
2334                var.name.starts_with(&prefix)
2335                    && var.name.ends_with(&suffix)
2336                    && var.name.len() >= prefix.len() + suffix.len()
2337            } else {
2338                var.name == pat
2339            }
2340        } else {
2341            true
2342        };
2343
2344        if matches_pattern && var.value.contains(search) {
2345            let new_value = var.value.replace(search, replacement);
2346            preview.push((var.name.clone(), var.value.clone(), new_value));
2347        }
2348    }
2349
2350    Ok(preview)
2351}
2352
2353/// Handle file watching and synchronization operations.
2354///
2355/// # Errors
2356///
2357/// This function will return an error if:
2358/// - Required output file is not specified for system-to-file or bidirectional sync
2359/// - Environment variable manager operations fail (loading, setting)
2360/// - Profile or project manager initialization fails
2361/// - File watcher creation or operation fails
2362/// - File I/O operations fail during synchronization
2363/// - Ctrl+C signal handler setup fails
2364/// - Change log export operations fail
2365/// - Invalid watch configuration is provided
2366/// - File system permissions prevent watching or writing to specified paths
2367pub fn handle_watch(args: &WatchArgs) -> Result<()> {
2368    // Validate arguments
2369    if matches!(args.direction, Direction::SystemToFile | Direction::Bidirectional) && args.output.is_none() {
2370        return Err(color_eyre::eyre::eyre!(
2371            "Output file required for system-to-file synchronization. Use --output <file>"
2372        ));
2373    }
2374
2375    let sync_mode = match args.direction {
2376        Direction::FileToSystem => SyncMode::FileToSystem,
2377        Direction::SystemToFile => SyncMode::SystemToFile,
2378        Direction::Bidirectional => SyncMode::Bidirectional,
2379    };
2380
2381    let mut config = WatchConfig {
2382        paths: if args.paths.is_empty() {
2383            vec![PathBuf::from(".")]
2384        } else {
2385            args.paths.clone()
2386        },
2387        mode: sync_mode,
2388        auto_reload: true,
2389        debounce_duration: Duration::from_millis(args.debounce),
2390        log_changes: !args.quiet,
2391        conflict_strategy: ConflictStrategy::UseLatest,
2392        ..Default::default()
2393    };
2394
2395    if !args.pattern.is_empty() {
2396        config.patterns.clone_from(&args.pattern);
2397    }
2398
2399    // Add output file to watch paths if bidirectional
2400    if let Some(output) = &args.output {
2401        if matches!(args.direction, Direction::Bidirectional) {
2402            config.paths.push(output.clone());
2403        }
2404    }
2405
2406    let mut manager = EnvVarManager::new();
2407    manager.load_all()?;
2408
2409    let mut watcher = EnvWatcher::new(config.clone(), manager);
2410
2411    // Set up the watcher with variable filtering
2412    if !args.vars.is_empty() {
2413        watcher.set_variable_filter(args.vars.clone());
2414    }
2415
2416    if let Some(output) = args.output.clone() {
2417        watcher.set_output_file(output);
2418    }
2419
2420    print_watch_header(args, &config);
2421
2422    watcher.start()?;
2423
2424    // Set up Ctrl+C handler
2425    let running = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
2426    let r = running.clone();
2427
2428    ctrlc::set_handler(move || {
2429        r.store(false, std::sync::atomic::Ordering::SeqCst);
2430    })?;
2431
2432    // Keep running until Ctrl+C
2433    while running.load(std::sync::atomic::Ordering::SeqCst) {
2434        std::thread::sleep(Duration::from_secs(1));
2435
2436        if let Some(log_file) = &args.log {
2437            let _ = watcher.export_change_log(log_file);
2438        }
2439    }
2440
2441    watcher.stop()?;
2442    println!("\nāœ… Watch mode stopped");
2443
2444    Ok(())
2445}
2446
2447fn print_watch_header(args: &WatchArgs, config: &WatchConfig) {
2448    println!("šŸ”„ Starting envx watch mode");
2449    println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2450
2451    match args.direction {
2452        Direction::FileToSystem => {
2453            println!("šŸ“‚ → šŸ’» Syncing from files to system");
2454            println!(
2455                "Watching: {}",
2456                config
2457                    .paths
2458                    .iter()
2459                    .map(|p| p.display().to_string())
2460                    .collect::<Vec<_>>()
2461                    .join(", ")
2462            );
2463        }
2464        Direction::SystemToFile => {
2465            println!("šŸ’» → šŸ“‚ Syncing from system to file");
2466            if let Some(output) = &args.output {
2467                println!("Output: {}", output.display());
2468            }
2469        }
2470        Direction::Bidirectional => {
2471            println!("šŸ“‚ ā†”ļø šŸ’» Bidirectional sync");
2472            println!(
2473                "Watching: {}",
2474                config
2475                    .paths
2476                    .iter()
2477                    .map(|p| p.display().to_string())
2478                    .collect::<Vec<_>>()
2479                    .join(", ")
2480            );
2481            if let Some(output) = &args.output {
2482                println!("Output: {}", output.display());
2483            }
2484        }
2485    }
2486
2487    if !args.vars.is_empty() {
2488        println!("Variables: {}", args.vars.join(", "));
2489    }
2490
2491    println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2492    println!("Press Ctrl+C to stop\n");
2493}