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        // Get hooks config (use defaults if not specified)
66        let hooks_config = spec.hooks.clone().unwrap_or_default();
67
68        // Ensure runtime client exists at root_dir (shared across all specs)
69        use crate::generator::writer::{ensure_directory, write_runtime_client};
70        let root_dir_path = PathBuf::from(&config.root_dir);
71        ensure_directory(&root_dir_path)?;
72        let runtime_dir = root_dir_path.join("runtime");
73        if !runtime_dir.exists() {
74            write_runtime_client(&root_dir_path, None, Some(apis_config))?;
75        }
76
77        // Print fetch message only once per unique URL
78        if !printed_urls.contains(spec_path) {
79            println!(
80                "{}",
81                format!("📥 Fetching spec from: {}", spec_path).bright_blue()
82            );
83            printed_urls.insert(spec_path.clone());
84        }
85
86        // Use caching for update command (same as generate)
87        let use_cache = config.generation.enable_cache;
88        let parsed = crate::generator::swagger_parser::fetch_and_parse_spec_with_cache_and_name(
89            spec_path,
90            use_cache,
91            Some(&spec.name),
92        )
93        .await?;
94
95        // Get selected modules from config, or select interactively if empty
96        let selected_modules = if modules_config.selected.is_empty() {
97            // No modules selected in config, select interactively
98            println!(
99                "{}",
100                format!(
101                    "No modules selected for spec '{}'. Please select modules to update:",
102                    spec.name
103                )
104                .bright_yellow()
105            );
106            println!();
107
108            // Filter out ignored modules
109            let available_modules: Vec<String> = parsed
110                .modules
111                .iter()
112                .filter(|m| !modules_config.ignore.contains(m))
113                .cloned()
114                .collect();
115
116            if available_modules.is_empty() {
117                println!(
118                    "{}",
119                    format!("⚠️  Skipping spec '{}': No modules available", spec.name).yellow()
120                );
121                continue;
122            }
123
124            // Select modules interactively
125            use crate::generator::module_selector::select_modules;
126            let selected = select_modules(&available_modules, &modules_config.ignore)?;
127
128            // Update config with selected modules
129            use crate::config::loader::load_config;
130            let mut config = load_config()?;
131            if let Some(spec_entry) = config.specs.iter_mut().find(|s| s.name == spec.name) {
132                spec_entry.modules.selected = selected.clone();
133            }
134            use crate::config::loader::save_config;
135            save_config(&config)?;
136
137            selected
138        } else {
139            modules_config.selected.clone()
140        };
141        println!(
142            "{}",
143            format!("✅ Parsed spec with {} modules", parsed.modules.len()).green()
144        );
145        println!();
146        println!(
147            "{}",
148            format!(
149                "📦 Updating {} module(s): {}",
150                selected_modules.len(),
151                selected_modules.join(", ")
152            )
153            .bright_green()
154        );
155        println!();
156
157        // Filter common schemas based on selected modules only
158        let (filtered_module_schemas, common_schemas) =
159            filter_common_schemas(&parsed.module_schemas, &selected_modules);
160
161        // Initialize template engine once for all modules
162        let project_root = std::env::current_dir().ok();
163        let template_engine =
164            crate::templates::engine::TemplateEngine::new(project_root.as_deref())?;
165
166        // Generate code for each module (using spec-specific or global output directories)
167        let schemas_dir = PathBuf::from(&schemas_config.output);
168        let apis_dir = PathBuf::from(&apis_config.output);
169
170        let mut total_files = 0;
171        let mut module_summary: Vec<(String, usize)> = Vec::new();
172
173        // Get force and backup settings from config
174        let use_force = config.generation.conflict_strategy == "force";
175        let use_backup = config.generation.enable_backup;
176
177        // Generate common module first if there are shared schemas
178        if !common_schemas.is_empty() {
179            println!("{}", "🔨 Regenerating common schemas...".bright_cyan());
180
181            // Shared enum registry to ensure consistent naming between TypeScript and Zod
182            let mut shared_enum_registry = std::collections::HashMap::new();
183
184            // Generate TypeScript typings for common schemas
185            // Pass empty common_schemas list so common schemas don't prefix themselves with "Common."
186            let common_types =
187                crate::generator::ts_typings::generate_typings_with_registry_and_engine_and_spec(
188                    &parsed.openapi,
189                    &parsed.schemas,
190                    &common_schemas,
191                    &mut shared_enum_registry,
192                    &[], // Empty list - common schemas shouldn't prefix themselves
193                    Some(&template_engine),
194                    Some(&spec.name),
195                )?;
196
197            // Generate Zod schemas for common schemas (using same registry)
198            // Pass empty common_schemas list so common schemas don't prefix themselves with "Common."
199            let common_zod_schemas =
200            crate::generator::zod_schema::generate_zod_schemas_with_registry_and_engine_and_spec(
201                &parsed.openapi,
202                &parsed.schemas,
203                &common_schemas,
204                &mut shared_enum_registry,
205                &[], // Empty list - common schemas shouldn't prefix themselves
206                Some(&template_engine),
207                Some(&spec.name),
208            )?;
209
210            // Write common schemas (use force if config says so)
211            use crate::generator::writer::write_schemas_with_module_mapping;
212            let common_files = write_schemas_with_module_mapping(
213                &schemas_dir,
214                "common",
215                &common_types,
216                &common_zod_schemas,
217                Some(&spec.name), // spec_name for multi-spec mode
218                use_backup,
219                use_force,
220                Some(&filtered_module_schemas),
221                &common_schemas,
222            )?;
223            total_files += common_files.len();
224            module_summary.push(("common".to_string(), common_files.len()));
225        }
226
227        for module in &selected_modules {
228            println!(
229                "{}",
230                format!("🔨 Regenerating code for module: {}", module).bright_cyan()
231            );
232
233            // Get operations for this module
234            let operations = parsed
235                .operations_by_tag
236                .get(module)
237                .cloned()
238                .unwrap_or_default();
239
240            if operations.is_empty() {
241                println!(
242                    "{}",
243                    format!("⚠️  No operations found for module: {}", module).yellow()
244                );
245                continue;
246            }
247
248            // Get schema names used by this module (from filtered schemas)
249            let module_schema_names = filtered_module_schemas
250                .get(module)
251                .cloned()
252                .unwrap_or_default();
253
254            // Shared enum registry to ensure consistent naming between TypeScript and Zod
255            let mut shared_enum_registry = std::collections::HashMap::new();
256
257            // Generate TypeScript typings
258            let types = if !module_schema_names.is_empty() {
259                crate::generator::ts_typings::generate_typings_with_registry_and_engine_and_spec(
260                    &parsed.openapi,
261                    &parsed.schemas,
262                    &module_schema_names,
263                    &mut shared_enum_registry,
264                    &common_schemas,
265                    Some(&template_engine),
266                    Some(&spec.name),
267                )?
268            } else {
269                Vec::new()
270            };
271
272            // Generate Zod schemas (using same registry)
273            let zod_schemas = if !module_schema_names.is_empty() {
274                crate::generator::zod_schema::generate_zod_schemas_with_registry_and_engine_and_spec(
275                &parsed.openapi,
276                &parsed.schemas,
277                &module_schema_names,
278                &mut shared_enum_registry,
279                &common_schemas,
280                Some(&template_engine),
281                Some(&spec.name),
282            )?
283            } else {
284                Vec::new()
285            };
286
287            // Generate query params types and Zod schemas
288            use crate::generator::query_params::{
289                generate_query_params_for_module, QueryParamsContext,
290            };
291            let query_params_result = generate_query_params_for_module(QueryParamsContext {
292                openapi: &parsed.openapi,
293                operations: &operations,
294                enum_registry: &mut shared_enum_registry,
295                template_engine: Some(&template_engine),
296                spec_name: Some(&spec.name),
297                existing_types: &types,
298                existing_zod_schemas: &zod_schemas,
299            })?;
300
301            // Generate API client (using same enum registry as schemas)
302            let api_result =
303            crate::generator::api_client::generate_api_client_with_registry_and_engine_and_spec(
304                &parsed.openapi,
305                &operations,
306                module,
307                &common_schemas,
308                &mut shared_enum_registry,
309                Some(&template_engine),
310                Some(&spec.name),
311                Some(&config.root_dir),
312                Some(&apis_config.output),
313                Some(&schemas_config.output),
314            )?;
315
316            // Response types are written to API files, not schema files
317            // Combine schema types with query params types
318            let mut all_types = types;
319            all_types.extend(query_params_result.types);
320
321            // Combine Zod schemas with query params Zod schemas
322            let mut all_zod_schemas = zod_schemas;
323            all_zod_schemas.extend(query_params_result.zod_schemas);
324
325            // Write schemas (use force if config says so)
326            use crate::generator::writer::write_schemas_with_module_mapping;
327            let schema_files = write_schemas_with_module_mapping(
328                &schemas_dir,
329                module,
330                &all_types,
331                &all_zod_schemas,
332                Some(&spec.name), // spec_name for multi-spec mode
333                use_backup,
334                use_force,
335                Some(&filtered_module_schemas),
336                &common_schemas,
337            )?;
338            total_files += schema_files.len();
339
340            // Write API client (use force if config says so)
341            let api_files = write_api_client_with_options(
342                &apis_dir,
343                module,
344                &api_result.functions,
345                Some(&spec.name), // spec_name for multi-spec mode
346                use_backup,
347                use_force,
348            )?;
349            total_files += api_files.len();
350
351            // Determine hook type from hooks config
352            use crate::specs::runner::HookType;
353            let hook_type = hooks_config
354                .library
355                .as_ref()
356                .and_then(|lib| match lib.as_str() {
357                    "react-query" => Some(HookType::ReactQuery),
358                    "swr" => Some(HookType::Swr),
359                    _ => None,
360                });
361
362            // Generate hooks if configured
363            let mut hook_files_count = 0;
364            if let Some(hook_type) = hook_type {
365                println!(
366                    "{}",
367                    format!("🔨 Generating hooks for module: {}", module).bright_cyan()
368                );
369
370                // Generate query keys first (hooks depend on them)
371                use crate::generator::query_keys::generate_query_keys;
372                let query_keys_context = generate_query_keys(&operations, module, Some(&spec.name));
373
374                // Render query keys template
375                let query_keys_content = template_engine.render(
376                    crate::templates::registry::TemplateId::QueryKeys,
377                    &query_keys_context,
378                )?;
379
380                // Write query keys file using configured output directory
381                // Note: output_dir already includes spec_name if needed (from config), just like schemas/apis
382                let query_keys_output = PathBuf::from(&hooks_config.query_keys_output);
383
384                use crate::generator::writer::write_query_keys_with_options;
385                write_query_keys_with_options(
386                    &query_keys_output,
387                    module,
388                    &query_keys_content,
389                    Some(&spec.name),
390                    use_backup,
391                    use_force,
392                )?;
393                total_files += 1;
394
395                // Generate hooks based on type
396                let hooks = match hook_type {
397                    HookType::ReactQuery => {
398                        use crate::generator::hooks::react_query::generate_react_query_hooks;
399                        generate_react_query_hooks(
400                            &parsed.openapi,
401                            &operations,
402                            module,
403                            Some(&spec.name),
404                            &common_schemas,
405                            &mut shared_enum_registry,
406                            &template_engine,
407                            Some(&apis_config.output),
408                            Some(&schemas_config.output),
409                            Some(&hooks_config.output),
410                            Some(&hooks_config.query_keys_output),
411                        )?
412                    }
413                    HookType::Swr => {
414                        use crate::generator::hooks::swr::generate_swr_hooks;
415                        generate_swr_hooks(
416                            &parsed.openapi,
417                            &operations,
418                            module,
419                            Some(&spec.name),
420                            &common_schemas,
421                            &mut shared_enum_registry,
422                            &template_engine,
423                            Some(&apis_config.output),
424                            Some(&schemas_config.output),
425                            Some(&hooks_config.output),
426                            Some(&hooks_config.query_keys_output),
427                        )?
428                    }
429                };
430
431                // Write hooks files using configured output directory
432                // Note: output_dir already includes spec_name if needed (from config), just like schemas/apis
433                let hooks_output = PathBuf::from(&hooks_config.output);
434
435                use crate::generator::writer::write_hooks_with_options;
436                let hook_files = write_hooks_with_options(
437                    &hooks_output,
438                    module,
439                    &hooks,
440                    Some(&spec.name),
441                    use_backup,
442                    use_force,
443                )?;
444                hook_files_count = hook_files.len();
445                total_files += hook_files_count;
446            }
447
448            let module_file_count = schema_files.len()
449                + api_files.len()
450                + if hook_type.is_some() {
451                    1 + hook_files_count
452                } else {
453                    0
454                };
455            module_summary.push((module.clone(), module_file_count));
456            println!(
457                "{}",
458                format!(
459                    "✅ Regenerated {} files for module: {}",
460                    module_file_count, module
461                )
462                .green()
463            );
464        }
465
466        println!();
467        println!(
468            "{}",
469            format!(
470                "✨ Successfully updated {} files for spec '{}'!",
471                total_files, spec.name
472            )
473            .bright_green()
474        );
475        println!();
476        println!(
477            "{}",
478            format!("Updated files for '{}':", spec.name).bright_cyan()
479        );
480        println!("  📁 Schemas: {}", schemas_config.output);
481        println!("  📁 APIs: {}", apis_config.output);
482        if hooks_config.library.is_some() {
483            println!("  📁 Hooks: {}", hooks_config.output);
484            println!("  📁 Query Keys: {}", hooks_config.query_keys_output);
485        }
486
487        // Store summary for this spec
488        all_specs_summary.push((spec.name.clone(), total_files, module_summary.clone()));
489
490        // Collect files for this spec for formatting
491        let current_dir = std::env::current_dir().map_err(|e| FileSystemError::ReadFileFailed {
492            path: ".".to_string(),
493            source: e,
494        })?;
495
496        let schemas_dir_abs = if schemas_dir.is_absolute() {
497            schemas_dir.clone()
498        } else {
499            current_dir.join(&schemas_dir)
500        };
501        let apis_dir_abs = if apis_dir.is_absolute() {
502            apis_dir.clone()
503        } else {
504            current_dir.join(&apis_dir)
505        };
506
507        // Collect schema files recursively
508        if schemas_dir_abs.exists() {
509            collect_ts_files(&schemas_dir_abs, &mut all_generated_files)?;
510        }
511
512        // Collect API files recursively
513        if apis_dir_abs.exists() {
514            collect_ts_files(&apis_dir_abs, &mut all_generated_files)?;
515        }
516    }
517
518    // Print overall summary
519    println!();
520    println!("{}", "=".repeat(60).bright_black());
521    println!();
522    let total_all_files: usize = all_specs_summary.iter().map(|(_, count, _)| count).sum();
523    println!(
524        "{}",
525        format!(
526            "✨ Successfully updated {} files across {} spec(s)!",
527            total_all_files,
528            all_specs_summary.len()
529        )
530        .bright_green()
531    );
532    println!();
533    println!("{}", "Summary by spec:".bright_cyan());
534    for (spec_name, file_count, module_summary) in &all_specs_summary {
535        println!("  📦 {}: {} files", spec_name, file_count);
536        if !module_summary.is_empty() {
537            for (module, count) in module_summary {
538                println!("    • {}: {} files", module, count);
539            }
540        }
541    }
542    println!();
543
544    // Format files if formatter is available
545    if !all_generated_files.is_empty() {
546        // Find the common parent directory (where config files are likely located)
547        // Try to find it from the first file path, or use current directory
548        let output_base = all_generated_files.first().and_then(|first_file| {
549            first_file
550                .parent()
551                .and_then(|p| p.parent())
552                .and_then(|p| p.parent())
553        });
554
555        let formatter = if let Some(base_dir) = output_base {
556            FormatterManager::detect_formatter_from_dir(base_dir)
557                .or_else(FormatterManager::detect_formatter)
558        } else {
559            FormatterManager::detect_formatter()
560        };
561
562        if let Some(formatter) = formatter {
563            println!("{}", "Formatting generated files...".bright_cyan());
564            let original_dir =
565                std::env::current_dir().map_err(|e| FileSystemError::ReadFileFailed {
566                    path: ".".to_string(),
567                    source: e,
568                })?;
569
570            if let Some(output_base) = output_base {
571                // Ensure output_base is not empty
572                if output_base.as_os_str().is_empty() {
573                    // Fallback: use current directory
574                    FormatterManager::format_files(&all_generated_files, formatter)?;
575                } else {
576                    std::env::set_current_dir(output_base).map_err(|e| {
577                        FileSystemError::ReadFileFailed {
578                            path: output_base.display().to_string(),
579                            source: e,
580                        }
581                    })?;
582
583                    // Convert paths to relative paths from output base directory
584                    let relative_files: Vec<PathBuf> = all_generated_files
585                        .iter()
586                        .filter_map(|p| {
587                            p.strip_prefix(output_base)
588                                .ok()
589                                .map(|p| p.to_path_buf())
590                                .filter(|p| !p.as_os_str().is_empty())
591                        })
592                        .collect();
593
594                    if !relative_files.is_empty() {
595                        let result = FormatterManager::format_files(&relative_files, formatter);
596
597                        // Restore original directory
598                        std::env::set_current_dir(&original_dir).map_err(|e| {
599                            FileSystemError::ReadFileFailed {
600                                path: original_dir.display().to_string(),
601                                source: e,
602                            }
603                        })?;
604
605                        result?;
606
607                        // Update metadata for formatted files to reflect formatted content hash (batch update)
608                        use crate::generator::writer::batch_update_file_metadata_from_disk;
609                        if let Err(e) = batch_update_file_metadata_from_disk(&all_generated_files) {
610                            // Log but don't fail - metadata update is best effort
611                            eprintln!("Warning: Failed to update metadata: {}", e);
612                        }
613                    } else {
614                        // Restore original directory
615                        std::env::set_current_dir(&original_dir).map_err(|e| {
616                            FileSystemError::ReadFileFailed {
617                                path: original_dir.display().to_string(),
618                                source: e,
619                            }
620                        })?;
621                    }
622                }
623            } else {
624                FormatterManager::format_files(&all_generated_files, formatter)?;
625
626                // Update metadata for formatted files to reflect formatted content hash (batch update)
627                use crate::generator::writer::batch_update_file_metadata_from_disk;
628                if let Err(e) = batch_update_file_metadata_from_disk(&all_generated_files) {
629                    // Log but don't fail - metadata update is best effort
630                    eprintln!("Warning: Failed to update metadata: {}", e);
631                }
632            }
633            println!("{}", "✅ Files formatted".green());
634        }
635    }
636
637    Ok(())
638}
639
640fn collect_ts_files(dir: &Path, files: &mut Vec<PathBuf>) -> Result<()> {
641    if dir.is_dir() {
642        for entry in std::fs::read_dir(dir).map_err(|e| FileSystemError::ReadFileFailed {
643            path: dir.display().to_string(),
644            source: e,
645        })? {
646            let entry = entry.map_err(|e| FileSystemError::ReadFileFailed {
647                path: dir.display().to_string(),
648                source: e,
649            })?;
650            let path = entry.path();
651            // Skip if path is empty or invalid
652            if path.as_os_str().is_empty() {
653                continue;
654            }
655            if path.is_dir() {
656                collect_ts_files(&path, files)?;
657            } else if path.extension().and_then(|s| s.to_str()) == Some("ts") {
658                files.push(path);
659            }
660        }
661    }
662    Ok(())
663}