1use 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#[derive(Debug, Args)]
12pub struct ConfigCommand {
13 #[command(subcommand)]
14 command: ConfigSubcommand,
15}
16
17#[derive(Debug, Subcommand)]
18enum ConfigSubcommand {
19 #[command(visible_aliases = &["check", "verify"])]
21 Validate {
22 #[arg(short = 'f', long)]
24 file: Option<std::path::PathBuf>,
25
26 #[arg(short = 's', long)]
28 show_suggestions: bool,
29 },
30
31 #[command(visible_aliases = &["repair", "fix"])]
33 AutoFix {
34 #[arg(short = 'f', long)]
36 file: Option<std::path::PathBuf>,
37
38 #[arg(short = 'd', long)]
40 dry_run: bool,
41 },
42
43 #[command(visible_aliases = &["upgrade", "update"])]
45 Migrate {
46 #[arg(short = 'f', long)]
48 from: std::path::PathBuf,
49
50 #[arg(short = 't', long)]
52 to: Option<std::path::PathBuf>,
53
54 #[arg(short = 'v', long)]
56 from_version: String,
57 },
58
59 #[command(visible_aliases = &["location", "path"])]
61 Show,
62
63 #[command(visible_aliases = &["create", "new"])]
65 Init {
66 #[arg(short = 'o', long)]
68 output: Option<std::path::PathBuf>,
69
70 #[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 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 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 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
298pub async fn handle_config_command(command: ConfigCommand, format: OutputFormat) -> Result<()> {
300 command.execute(format).await
301}