vika_cli/commands/
update.rs

1use crate::config::loader::load_config;
2use crate::config::validator::validate_config;
3use crate::error::{FileSystemError, Result};
4use crate::formatter::FormatterManager;
5use crate::generator::swagger_parser::filter_common_schemas;
6use crate::generator::writer::write_api_client_with_options;
7use colored::*;
8use std::path::{Path, PathBuf};
9
10pub async fn run() -> Result<()> {
11    println!("{}", "🔄 Updating generated code...".bright_cyan());
12    println!();
13
14    // Load config
15    let config = load_config()?;
16    validate_config(&config)?;
17
18    use crate::error::{FileSystemError, GenerationError};
19    use crate::specs::manager::list_specs;
20
21    // Get specs from config
22    let specs = list_specs(&config);
23    if specs.is_empty() {
24        return Err(GenerationError::SpecPathRequired.into());
25    }
26
27    // Update all specs
28    type SpecSummary = (String, usize, Vec<(String, usize)>);
29    let mut all_specs_summary: Vec<SpecSummary> = Vec::new();
30    let mut all_generated_files = Vec::new();
31    // Track which URLs we've already printed the fetch message for
32    let mut printed_urls: std::collections::HashSet<String> = std::collections::HashSet::new();
33
34    for spec in &specs {
35        println!();
36        println!(
37            "{}",
38            format!("🔄 Updating spec: {}", spec.name).bright_cyan()
39        );
40        println!();
41
42        let spec_path = &spec.path;
43
44        // Check if spec path is a temporary file that might not exist anymore
45        if !spec_path.starts_with("http://")
46            && !spec_path.starts_with("https://")
47            && !std::path::Path::new(spec_path).exists()
48        {
49            println!(
50                "{}",
51                format!(
52                    "⚠️  Skipping spec '{}': spec file no longer exists at {}",
53                    spec.name, spec_path
54                )
55                .yellow()
56            );
57            continue;
58        }
59
60        // Use spec-specific configs (required per spec)
61        let schemas_config = &spec.schemas;
62        let apis_config = &spec.apis;
63        let modules_config = &spec.modules;
64
65        // Ensure http.ts exists for this spec
66        use crate::generator::writer::{ensure_directory, write_http_client_template};
67        let apis_dir = PathBuf::from(&apis_config.output);
68        ensure_directory(&apis_dir)?;
69        let http_file = apis_dir.join("http.ts");
70        if !http_file.exists() {
71            write_http_client_template(&http_file)?;
72        }
73
74        // Print fetch message only once per unique URL
75        if !printed_urls.contains(spec_path) {
76            println!(
77                "{}",
78                format!("📥 Fetching spec from: {}", spec_path).bright_blue()
79            );
80            printed_urls.insert(spec_path.clone());
81        }
82
83        // Use caching for update command (same as generate)
84        let use_cache = config.generation.enable_cache;
85        let parsed = crate::generator::swagger_parser::fetch_and_parse_spec_with_cache_and_name(
86            spec_path,
87            use_cache,
88            Some(&spec.name),
89        )
90        .await?;
91
92        // Get selected modules from config, or select interactively if empty
93        let selected_modules = if modules_config.selected.is_empty() {
94            // No modules selected in config, select interactively
95            println!(
96                "{}",
97                format!(
98                    "No modules selected for spec '{}'. Please select modules to update:",
99                    spec.name
100                )
101                .bright_yellow()
102            );
103            println!();
104
105            // Filter out ignored modules
106            let available_modules: Vec<String> = parsed
107                .modules
108                .iter()
109                .filter(|m| !modules_config.ignore.contains(m))
110                .cloned()
111                .collect();
112
113            if available_modules.is_empty() {
114                println!(
115                    "{}",
116                    format!("⚠️  Skipping spec '{}': No modules available", spec.name).yellow()
117                );
118                continue;
119            }
120
121            // Select modules interactively
122            use crate::generator::module_selector::select_modules;
123            let selected = select_modules(&available_modules, &modules_config.ignore)?;
124
125            // Update config with selected modules
126            use crate::config::loader::load_config;
127            let mut config = load_config()?;
128            if let Some(spec_entry) = config.specs.iter_mut().find(|s| s.name == spec.name) {
129                spec_entry.modules.selected = selected.clone();
130            }
131            use crate::config::loader::save_config;
132            save_config(&config)?;
133
134            selected
135        } else {
136            modules_config.selected.clone()
137        };
138        println!(
139            "{}",
140            format!("✅ Parsed spec with {} modules", parsed.modules.len()).green()
141        );
142        println!();
143        println!(
144            "{}",
145            format!(
146                "📦 Updating {} module(s): {}",
147                selected_modules.len(),
148                selected_modules.join(", ")
149            )
150            .bright_green()
151        );
152        println!();
153
154        // Filter common schemas based on selected modules only
155        let (filtered_module_schemas, common_schemas) =
156            filter_common_schemas(&parsed.module_schemas, &selected_modules);
157
158        // Initialize template engine once for all modules
159        let project_root = std::env::current_dir().ok();
160        let template_engine =
161            crate::templates::engine::TemplateEngine::new(project_root.as_deref())?;
162
163        // Generate code for each module (using spec-specific or global output directories)
164        let schemas_dir = PathBuf::from(&schemas_config.output);
165        let apis_dir = PathBuf::from(&apis_config.output);
166
167        let mut total_files = 0;
168        let mut module_summary: Vec<(String, usize)> = Vec::new();
169
170        // Get force and backup settings from config
171        let use_force = config.generation.conflict_strategy == "force";
172        let use_backup = config.generation.enable_backup;
173
174        // Generate common module first if there are shared schemas
175        if !common_schemas.is_empty() {
176            println!("{}", "🔨 Regenerating common schemas...".bright_cyan());
177
178            // Shared enum registry to ensure consistent naming between TypeScript and Zod
179            let mut shared_enum_registry = std::collections::HashMap::new();
180
181            // Generate TypeScript typings for common schemas
182            // Pass empty common_schemas list so common schemas don't prefix themselves with "Common."
183            let common_types =
184                crate::generator::ts_typings::generate_typings_with_registry_and_engine_and_spec(
185                    &parsed.openapi,
186                    &parsed.schemas,
187                    &common_schemas,
188                    &mut shared_enum_registry,
189                    &[], // Empty list - common schemas shouldn't prefix themselves
190                    Some(&template_engine),
191                    Some(&spec.name),
192                )?;
193
194            // Generate Zod schemas for common schemas (using same registry)
195            // Pass empty common_schemas list so common schemas don't prefix themselves with "Common."
196            let common_zod_schemas =
197            crate::generator::zod_schema::generate_zod_schemas_with_registry_and_engine_and_spec(
198                &parsed.openapi,
199                &parsed.schemas,
200                &common_schemas,
201                &mut shared_enum_registry,
202                &[], // Empty list - common schemas shouldn't prefix themselves
203                Some(&template_engine),
204                Some(&spec.name),
205            )?;
206
207            // Write common schemas (use force if config says so)
208            use crate::generator::writer::write_schemas_with_module_mapping;
209            let common_files = write_schemas_with_module_mapping(
210                &schemas_dir,
211                "common",
212                &common_types,
213                &common_zod_schemas,
214                Some(&spec.name), // spec_name for multi-spec mode
215                use_backup,
216                use_force,
217                Some(&filtered_module_schemas),
218                &common_schemas,
219            )?;
220            total_files += common_files.len();
221            module_summary.push(("common".to_string(), common_files.len()));
222        }
223
224        for module in &selected_modules {
225            println!(
226                "{}",
227                format!("🔨 Regenerating code for module: {}", module).bright_cyan()
228            );
229
230            // Get operations for this module
231            let operations = parsed
232                .operations_by_tag
233                .get(module)
234                .cloned()
235                .unwrap_or_default();
236
237            if operations.is_empty() {
238                println!(
239                    "{}",
240                    format!("⚠️  No operations found for module: {}", module).yellow()
241                );
242                continue;
243            }
244
245            // Get schema names used by this module (from filtered schemas)
246            let module_schema_names = filtered_module_schemas
247                .get(module)
248                .cloned()
249                .unwrap_or_default();
250
251            // Shared enum registry to ensure consistent naming between TypeScript and Zod
252            let mut shared_enum_registry = std::collections::HashMap::new();
253
254            // Generate TypeScript typings
255            let types = if !module_schema_names.is_empty() {
256                crate::generator::ts_typings::generate_typings_with_registry_and_engine_and_spec(
257                    &parsed.openapi,
258                    &parsed.schemas,
259                    &module_schema_names,
260                    &mut shared_enum_registry,
261                    &common_schemas,
262                    Some(&template_engine),
263                    Some(&spec.name),
264                )?
265            } else {
266                Vec::new()
267            };
268
269            // Generate Zod schemas (using same registry)
270            let zod_schemas = if !module_schema_names.is_empty() {
271                crate::generator::zod_schema::generate_zod_schemas_with_registry_and_engine_and_spec(
272                &parsed.openapi,
273                &parsed.schemas,
274                &module_schema_names,
275                &mut shared_enum_registry,
276                &common_schemas,
277                Some(&template_engine),
278                Some(&spec.name),
279            )?
280            } else {
281                Vec::new()
282            };
283
284            // Generate API client (using same enum registry as schemas)
285            let api_result =
286            crate::generator::api_client::generate_api_client_with_registry_and_engine_and_spec(
287                &parsed.openapi,
288                &operations,
289                module,
290                &common_schemas,
291                &mut shared_enum_registry,
292                Some(&template_engine),
293                Some(&spec.name),
294            )?;
295
296            // Combine response types with schema types
297            let mut all_types = types;
298            all_types.extend(api_result.response_types);
299
300            // Write schemas (use force if config says so)
301            use crate::generator::writer::write_schemas_with_module_mapping;
302            let schema_files = write_schemas_with_module_mapping(
303                &schemas_dir,
304                module,
305                &all_types,
306                &zod_schemas,
307                Some(&spec.name), // spec_name for multi-spec mode
308                use_backup,
309                use_force,
310                Some(&filtered_module_schemas),
311                &common_schemas,
312            )?;
313            total_files += schema_files.len();
314
315            // Write API client (use force if config says so)
316            let api_files = write_api_client_with_options(
317                &apis_dir,
318                module,
319                &api_result.functions,
320                Some(&spec.name), // spec_name for multi-spec mode
321                use_backup,
322                use_force,
323            )?;
324            total_files += api_files.len();
325
326            let module_file_count = schema_files.len() + api_files.len();
327            module_summary.push((module.clone(), module_file_count));
328            println!(
329                "{}",
330                format!(
331                    "✅ Regenerated {} files for module: {}",
332                    module_file_count, module
333                )
334                .green()
335            );
336        }
337
338        println!();
339        println!(
340            "{}",
341            format!(
342                "✨ Successfully updated {} files for spec '{}'!",
343                total_files, spec.name
344            )
345            .bright_green()
346        );
347        println!();
348        println!(
349            "{}",
350            format!("Updated files for '{}':", spec.name).bright_cyan()
351        );
352        println!("  📁 Schemas: {}", schemas_config.output);
353        println!("  📁 APIs: {}", apis_config.output);
354
355        // Store summary for this spec
356        all_specs_summary.push((spec.name.clone(), total_files, module_summary.clone()));
357
358        // Collect files for this spec for formatting
359        let current_dir = std::env::current_dir().map_err(|e| FileSystemError::ReadFileFailed {
360            path: ".".to_string(),
361            source: e,
362        })?;
363
364        let schemas_dir_abs = if schemas_dir.is_absolute() {
365            schemas_dir.clone()
366        } else {
367            current_dir.join(&schemas_dir)
368        };
369        let apis_dir_abs = if apis_dir.is_absolute() {
370            apis_dir.clone()
371        } else {
372            current_dir.join(&apis_dir)
373        };
374
375        // Collect schema files recursively
376        if schemas_dir_abs.exists() {
377            collect_ts_files(&schemas_dir_abs, &mut all_generated_files)?;
378        }
379
380        // Collect API files recursively
381        if apis_dir_abs.exists() {
382            collect_ts_files(&apis_dir_abs, &mut all_generated_files)?;
383        }
384    }
385
386    // Print overall summary
387    println!();
388    println!("{}", "=".repeat(60).bright_black());
389    println!();
390    let total_all_files: usize = all_specs_summary.iter().map(|(_, count, _)| count).sum();
391    println!(
392        "{}",
393        format!(
394            "✨ Successfully updated {} files across {} spec(s)!",
395            total_all_files,
396            all_specs_summary.len()
397        )
398        .bright_green()
399    );
400    println!();
401    println!("{}", "Summary by spec:".bright_cyan());
402    for (spec_name, file_count, module_summary) in &all_specs_summary {
403        println!("  📦 {}: {} files", spec_name, file_count);
404        if !module_summary.is_empty() {
405            for (module, count) in module_summary {
406                println!("    • {}: {} files", module, count);
407            }
408        }
409    }
410    println!();
411
412    // Format files if formatter is available
413    if !all_generated_files.is_empty() {
414        // Find the common parent directory (where config files are likely located)
415        // Try to find it from the first file path, or use current directory
416        let output_base = all_generated_files.first().and_then(|first_file| {
417            first_file
418                .parent()
419                .and_then(|p| p.parent())
420                .and_then(|p| p.parent())
421        });
422
423        let formatter = if let Some(base_dir) = output_base {
424            FormatterManager::detect_formatter_from_dir(base_dir)
425                .or_else(FormatterManager::detect_formatter)
426        } else {
427            FormatterManager::detect_formatter()
428        };
429
430        if let Some(formatter) = formatter {
431            println!("{}", "Formatting generated files...".bright_cyan());
432            let original_dir =
433                std::env::current_dir().map_err(|e| FileSystemError::ReadFileFailed {
434                    path: ".".to_string(),
435                    source: e,
436                })?;
437
438            if let Some(output_base) = output_base {
439                // Ensure output_base is not empty
440                if output_base.as_os_str().is_empty() {
441                    // Fallback: use current directory
442                    FormatterManager::format_files(&all_generated_files, formatter)?;
443                } else {
444                    std::env::set_current_dir(output_base).map_err(|e| {
445                        FileSystemError::ReadFileFailed {
446                            path: output_base.display().to_string(),
447                            source: e,
448                        }
449                    })?;
450
451                    // Convert paths to relative paths from output base directory
452                    let relative_files: Vec<PathBuf> = all_generated_files
453                        .iter()
454                        .filter_map(|p| {
455                            p.strip_prefix(output_base)
456                                .ok()
457                                .map(|p| p.to_path_buf())
458                                .filter(|p| !p.as_os_str().is_empty())
459                        })
460                        .collect();
461
462                    if !relative_files.is_empty() {
463                        let result = FormatterManager::format_files(&relative_files, formatter);
464
465                        // Restore original directory
466                        std::env::set_current_dir(&original_dir).map_err(|e| {
467                            FileSystemError::ReadFileFailed {
468                                path: original_dir.display().to_string(),
469                                source: e,
470                            }
471                        })?;
472
473                        result?;
474
475                        // Update metadata for formatted files to reflect formatted content hash (batch update)
476                        use crate::generator::writer::batch_update_file_metadata_from_disk;
477                        if let Err(e) = batch_update_file_metadata_from_disk(&all_generated_files) {
478                            // Log but don't fail - metadata update is best effort
479                            eprintln!("Warning: Failed to update metadata: {}", e);
480                        }
481                    } else {
482                        // Restore original directory
483                        std::env::set_current_dir(&original_dir).map_err(|e| {
484                            FileSystemError::ReadFileFailed {
485                                path: original_dir.display().to_string(),
486                                source: e,
487                            }
488                        })?;
489                    }
490                }
491            } else {
492                FormatterManager::format_files(&all_generated_files, formatter)?;
493
494                // Update metadata for formatted files to reflect formatted content hash (batch update)
495                use crate::generator::writer::batch_update_file_metadata_from_disk;
496                if let Err(e) = batch_update_file_metadata_from_disk(&all_generated_files) {
497                    // Log but don't fail - metadata update is best effort
498                    eprintln!("Warning: Failed to update metadata: {}", e);
499                }
500            }
501            println!("{}", "✅ Files formatted".green());
502        }
503    }
504
505    Ok(())
506}
507
508fn collect_ts_files(dir: &Path, files: &mut Vec<PathBuf>) -> Result<()> {
509    if dir.is_dir() {
510        for entry in std::fs::read_dir(dir).map_err(|e| FileSystemError::ReadFileFailed {
511            path: dir.display().to_string(),
512            source: e,
513        })? {
514            let entry = entry.map_err(|e| FileSystemError::ReadFileFailed {
515                path: dir.display().to_string(),
516                source: e,
517            })?;
518            let path = entry.path();
519            // Skip if path is empty or invalid
520            if path.as_os_str().is_empty() {
521                continue;
522            }
523            if path.is_dir() {
524                collect_ts_files(&path, files)?;
525            } else if path.extension().and_then(|s| s.to_str()) == Some("ts") {
526                files.push(path);
527            }
528        }
529    }
530    Ok(())
531}