mielin_cli/commands/
config.rs

1//! Configuration management command implementations
2
3use crate::config::Config;
4use crate::config_validator::{ConfigMigrator, ConfigValidator, ErrorSeverity};
5use crate::output::OutputFormat;
6use anyhow::Result;
7use clap::{Args, Subcommand};
8use comfy_table::{presets::UTF8_FULL, Cell, Color, ContentArrangement, Table};
9
10/// Configuration management commands
11#[derive(Debug, Args)]
12pub struct ConfigCommand {
13    #[command(subcommand)]
14    command: ConfigSubcommand,
15}
16
17#[derive(Debug, Subcommand)]
18enum ConfigSubcommand {
19    /// Validate configuration file
20    #[command(visible_aliases = &["check", "verify"])]
21    Validate {
22        /// Path to configuration file (uses default if not specified)
23        #[arg(short = 'f', long)]
24        file: Option<std::path::PathBuf>,
25
26        /// Show suggestions even if config is valid
27        #[arg(short = 's', long)]
28        show_suggestions: bool,
29    },
30
31    /// Auto-fix configuration issues
32    #[command(visible_aliases = &["repair", "fix"])]
33    AutoFix {
34        /// Path to configuration file (uses default if not specified)
35        #[arg(short = 'f', long)]
36        file: Option<std::path::PathBuf>,
37
38        /// Dry run (show fixes without applying them)
39        #[arg(short = 'd', long)]
40        dry_run: bool,
41    },
42
43    /// Migrate configuration to newer version
44    #[command(visible_aliases = &["upgrade", "update"])]
45    Migrate {
46        /// Source configuration file
47        #[arg(short = 'f', long)]
48        from: std::path::PathBuf,
49
50        /// Destination file (overwrites source if not specified)
51        #[arg(short = 't', long)]
52        to: Option<std::path::PathBuf>,
53
54        /// Source version
55        #[arg(short = 'v', long)]
56        from_version: String,
57    },
58
59    /// Show configuration file path
60    #[command(visible_aliases = &["location", "path"])]
61    Show,
62
63    /// Initialize a new configuration file
64    #[command(visible_aliases = &["create", "new"])]
65    Init {
66        /// Output path (uses default if not specified)
67        #[arg(short = 'o', long)]
68        output: Option<std::path::PathBuf>,
69
70        /// Force overwrite if file exists
71        #[arg(short = 'f', long)]
72        force: bool,
73    },
74}
75
76impl ConfigCommand {
77    pub async fn execute(&self, output_format: OutputFormat) -> Result<()> {
78        match &self.command {
79            ConfigSubcommand::Validate {
80                file,
81                show_suggestions,
82            } => validate_command(file.as_deref(), *show_suggestions, output_format).await,
83            ConfigSubcommand::AutoFix { file, dry_run } => {
84                auto_fix_command(file.as_deref(), *dry_run).await
85            }
86            ConfigSubcommand::Migrate {
87                from,
88                to,
89                from_version,
90            } => migrate_command(from, to.as_deref(), from_version).await,
91            ConfigSubcommand::Show => show_command().await,
92            ConfigSubcommand::Init { output, force } => {
93                init_command(output.as_deref(), *force).await
94            }
95        }
96    }
97}
98
99async fn validate_command(
100    file: Option<&std::path::Path>,
101    show_suggestions: bool,
102    format: OutputFormat,
103) -> Result<()> {
104    let validator = if let Some(path) = file {
105        ConfigValidator::from_file(path)?
106    } else {
107        let config = Config::load()?;
108        ConfigValidator::new(config)
109    };
110
111    let result = validator.validate();
112
113    match format {
114        OutputFormat::Json => {
115            println!("{}", serde_json::to_string_pretty(&result)?);
116        }
117        OutputFormat::Yaml => {
118            println!("{}", serde_yaml::to_string(&result)?);
119        }
120        OutputFormat::Quiet => {
121            if result.is_valid {
122                println!("valid");
123            } else {
124                println!("invalid");
125                std::process::exit(1);
126            }
127        }
128        OutputFormat::Table => {
129            if result.is_valid {
130                println!("✓ Configuration is valid");
131            } else {
132                println!("✗ Configuration has errors");
133            }
134            println!();
135
136            // Show errors
137            if !result.errors.is_empty() {
138                println!("Errors:");
139                let mut table = Table::new();
140                table
141                    .load_preset(UTF8_FULL)
142                    .set_content_arrangement(ContentArrangement::Dynamic)
143                    .set_header(vec!["Field", "Severity", "Message"]);
144
145                for error in &result.errors {
146                    let severity_cell = match error.severity {
147                        ErrorSeverity::Critical => Cell::new("Critical").fg(Color::Red),
148                        ErrorSeverity::Error => Cell::new("Error").fg(Color::Yellow),
149                    };
150
151                    table.add_row(vec![
152                        Cell::new(&error.field),
153                        severity_cell,
154                        Cell::new(&error.message),
155                    ]);
156                }
157
158                println!("{}", table);
159                println!();
160            }
161
162            // Show warnings
163            if !result.warnings.is_empty() {
164                println!("Warnings:");
165                let mut table = Table::new();
166                table
167                    .load_preset(UTF8_FULL)
168                    .set_content_arrangement(ContentArrangement::Dynamic)
169                    .set_header(vec!["Field", "Message"]);
170
171                for warning in &result.warnings {
172                    table.add_row(vec![Cell::new(&warning.field), Cell::new(&warning.message)]);
173                }
174
175                println!("{}", table);
176                println!();
177            }
178
179            // Show suggestions
180            if (show_suggestions || !result.is_valid) && !result.suggestions.is_empty() {
181                println!("Suggestions:");
182                let mut table = Table::new();
183                table
184                    .load_preset(UTF8_FULL)
185                    .set_content_arrangement(ContentArrangement::Dynamic)
186                    .set_header(vec!["Field", "Suggested Value", "Reason"]);
187
188                for suggestion in &result.suggestions {
189                    table.add_row(vec![
190                        Cell::new(&suggestion.field),
191                        Cell::new(&suggestion.suggested_value),
192                        Cell::new(&suggestion.reason),
193                    ]);
194                }
195
196                println!("{}", table);
197            }
198
199            if !result.is_valid {
200                std::process::exit(1);
201            }
202        }
203    }
204
205    Ok(())
206}
207
208async fn auto_fix_command(file: Option<&std::path::Path>, dry_run: bool) -> Result<()> {
209    let path = if let Some(p) = file {
210        p.to_path_buf()
211    } else {
212        Config::default_path()
213    };
214
215    let mut validator = ConfigValidator::from_file(&path)?;
216    let fixes = validator.auto_fix();
217
218    if fixes.is_empty() {
219        println!("✓ No issues to fix");
220        return Ok(());
221    }
222
223    println!("Applied {} fixes:", fixes.len());
224    for fix in &fixes {
225        println!("  • {}", fix);
226    }
227
228    if dry_run {
229        println!("\nDry run mode - no changes were saved");
230    } else {
231        validator.save(&path)?;
232        println!("\n✓ Configuration saved to {}", path.display());
233    }
234
235    Ok(())
236}
237
238async fn migrate_command(
239    from: &std::path::Path,
240    to: Option<&std::path::Path>,
241    from_version: &str,
242) -> Result<()> {
243    let mut config = Config::load_from_path(from)?;
244    let changes = ConfigMigrator::migrate(from_version, &mut config)?;
245
246    println!("Migration completed:");
247    for change in &changes {
248        println!("  • {}", change);
249    }
250
251    let target_path = to.unwrap_or(from);
252    config.save_to_path(target_path)?;
253
254    println!(
255        "\n✓ Migrated configuration saved to {}",
256        target_path.display()
257    );
258
259    Ok(())
260}
261
262async fn show_command() -> Result<()> {
263    let path = Config::default_path();
264    println!("Configuration file: {}", path.display());
265    println!("Exists: {}", path.exists());
266
267    if path.exists() {
268        let metadata = std::fs::metadata(&path)?;
269        println!("Size: {} bytes", metadata.len());
270        println!("Modified: {:?}", metadata.modified()?);
271    }
272
273    Ok(())
274}
275
276async fn init_command(output: Option<&std::path::Path>, force: bool) -> Result<()> {
277    let path = if let Some(p) = output {
278        p.to_path_buf()
279    } else {
280        Config::default_path()
281    };
282
283    if path.exists() && !force {
284        anyhow::bail!(
285            "Configuration file already exists at {}. Use --force to overwrite",
286            path.display()
287        );
288    }
289
290    let config = Config::default();
291    config.save_to_path(&path)?;
292
293    println!("✓ Created configuration file at {}", path.display());
294
295    Ok(())
296}
297
298/// Handle config command
299pub async fn handle_config_command(command: ConfigCommand, format: OutputFormat) -> Result<()> {
300    command.execute(format).await
301}