vika_cli/commands/
add.rs

1use crate::config::loader::{load_config, save_config};
2use crate::config::validator::validate_config;
3use crate::error::{GenerationError, Result};
4use crate::generator::writer::{ensure_directory, write_runtime_client};
5use colored::*;
6use dialoguer::{Confirm, Input, Select};
7use std::path::PathBuf;
8
9pub async fn run() -> Result<()> {
10    println!("{}", "Adding new spec to vika-cli project...".bright_cyan());
11    println!();
12
13    // Check if config exists
14    let config_path = PathBuf::from(".vika.json");
15    if !config_path.exists() {
16        return Err(GenerationError::InvalidOperation {
17            message: ".vika.json not found. Please run 'vika-cli init' first.".to_string(),
18        }
19        .into());
20    }
21
22    // Load existing config
23    let mut config = load_config()?;
24    validate_config(&config)?;
25
26    println!("{}", "Let's configure your new spec:".bright_cyan());
27    println!();
28
29    // Collect spec details
30    let spec_name: String = Input::new()
31        .with_prompt("Spec name (kebab-case recommended, e.g., 'ecommerce', 'auth-api')")
32        .validate_with(|input: &String| -> std::result::Result<(), String> {
33            if input.trim().is_empty() {
34                return Err("Spec name cannot be empty".to_string());
35            }
36            // Check if spec name already exists
37            if config.specs.iter().any(|s| s.name == input.trim()) {
38                return Err(format!("Spec '{}' already exists", input.trim()));
39            }
40            Ok(())
41        })
42        .interact_text()
43        .map_err(|e| GenerationError::InvalidOperation {
44            message: format!("Failed to get user input: {}", e),
45        })?;
46
47    let spec_path_input: String = Input::new()
48        .with_prompt(format!("Path or URL for '{}'", spec_name.trim()))
49        .interact_text()
50        .map_err(|e| GenerationError::InvalidOperation {
51            message: format!("Failed to get user input: {}", e),
52        })?;
53
54    println!();
55    println!(
56        "{}",
57        format!("📋 Configuration for spec '{}'", spec_name.trim()).bright_cyan()
58    );
59    println!();
60
61    // Per-spec schemas config
62    println!("{}", "📁 Schemas Configuration".bright_yellow());
63    println!();
64
65    let spec_schemas_output: String = Input::new()
66        .with_prompt(format!(
67            "Schemas output directory for '{}'",
68            spec_name.trim()
69        ))
70        .default(format!("src/schemas/{}", spec_name.trim()))
71        .interact_text()
72        .map_err(|e| GenerationError::InvalidOperation {
73            message: format!("Failed to get user input: {}", e),
74        })?;
75
76    println!();
77
78    let spec_naming_options = ["PascalCase", "camelCase", "snake_case", "kebab-case"];
79    let spec_naming_index = Select::new()
80        .with_prompt(format!(
81            "Schema naming convention for '{}'",
82            spec_name.trim()
83        ))
84        .items(&[
85            "PascalCase - ProductDto, UserProfile (recommended)",
86            "camelCase - productDto, userProfile",
87            "snake_case - product_dto, user_profile",
88            "kebab-case - product-dto, user-profile",
89        ])
90        .default(0)
91        .interact()
92        .map_err(|e| GenerationError::InvalidOperation {
93            message: format!("Failed to get user selection: {}", e),
94        })?;
95    let spec_naming = spec_naming_options[spec_naming_index].to_string();
96
97    println!();
98
99    // Per-spec APIs config
100    println!("{}", "🔌 API Configuration".bright_yellow());
101    println!();
102
103    let spec_apis_output: String = Input::new()
104        .with_prompt(format!("APIs output directory for '{}'", spec_name.trim()))
105        .default(format!("src/apis/{}", spec_name.trim()))
106        .interact_text()
107        .map_err(|e| GenerationError::InvalidOperation {
108            message: format!("Failed to get user input: {}", e),
109        })?;
110
111    println!();
112
113    let spec_api_style_options = ["fetch"];
114    let spec_api_style_index = Select::new()
115        .with_prompt(format!("API client style for '{}'", spec_name.trim()))
116        .items(&["fetch - Native Fetch API (recommended)"])
117        .default(0)
118        .interact()
119        .map_err(|e| GenerationError::InvalidOperation {
120            message: format!("Failed to get user selection: {}", e),
121        })?;
122    let spec_api_style = spec_api_style_options[spec_api_style_index].to_string();
123
124    println!();
125
126    let spec_base_url_input: String = Input::new()
127        .with_prompt(format!(
128            "API base URL for '{}' (optional, press Enter to skip)",
129            spec_name.trim()
130        ))
131        .allow_empty(true)
132        .interact_text()
133        .map_err(|e| GenerationError::InvalidOperation {
134            message: format!("Failed to get user input: {}", e),
135        })?;
136
137    let spec_base_url = if spec_base_url_input.trim().is_empty() {
138        None
139    } else {
140        Some(spec_base_url_input.trim().to_string())
141    };
142
143    println!();
144
145    let spec_header_strategy_options = ["consumerInjected", "bearerToken", "fixed"];
146    let spec_header_strategy_index = Select::new()
147        .with_prompt(format!("Header strategy for '{}'", spec_name.trim()))
148        .items(&[
149            "consumerInjected - Headers provided by consumer (recommended)",
150            "bearerToken - Automatic Bearer token injection",
151            "fixed - Fixed headers from config",
152        ])
153        .default(0)
154        .interact()
155        .map_err(|e| GenerationError::InvalidOperation {
156            message: format!("Failed to get user selection: {}", e),
157        })?;
158    let spec_header_strategy = spec_header_strategy_options[spec_header_strategy_index].to_string();
159
160    println!();
161
162    // Hooks configuration
163    println!("{}", "📎 Hooks Configuration".bright_yellow());
164    println!();
165
166    let enable_hooks = Confirm::new()
167        .with_prompt("Do you want to generate hooks (React Query or SWR)?")
168        .default(false)
169        .interact()
170        .map_err(|e| GenerationError::InvalidOperation {
171            message: format!("Failed to get user input: {}", e),
172        })?;
173
174    let hooks_config = if enable_hooks {
175        let hook_library_options = ["react-query", "swr"];
176        let hook_library_index = Select::new()
177            .with_prompt("Which hook library do you want to use?")
178            .items(&hook_library_options)
179            .default(0)
180            .interact()
181            .map_err(|e| GenerationError::InvalidOperation {
182                message: format!("Failed to get user selection: {}", e),
183            })?;
184        let hook_library = hook_library_options[hook_library_index].to_string();
185
186        let hooks_output: String = Input::new()
187            .with_prompt("Hooks output directory")
188            .default("src/hooks".to_string())
189            .interact_text()
190            .map_err(|e| GenerationError::InvalidOperation {
191                message: format!("Failed to get user input: {}", e),
192            })?;
193
194        let query_keys_output: String = Input::new()
195            .with_prompt("Query keys output directory")
196            .default("src/query-keys".to_string())
197            .interact_text()
198            .map_err(|e| GenerationError::InvalidOperation {
199                message: format!("Failed to get user input: {}", e),
200            })?;
201
202        Some(crate::config::model::HooksConfig {
203            output: hooks_output.trim().to_string(),
204            query_keys_output: query_keys_output.trim().to_string(),
205            library: Some(hook_library),
206        })
207    } else {
208        None
209    };
210
211    println!();
212
213    // Create the new spec entry
214    let new_spec = crate::config::model::SpecEntry {
215        name: spec_name.trim().to_string(),
216        path: spec_path_input.trim().to_string(),
217        schemas: crate::config::model::SchemasConfig {
218            output: spec_schemas_output.trim().to_string(),
219            naming: spec_naming,
220        },
221        apis: crate::config::model::ApisConfig {
222            output: spec_apis_output.trim().to_string(),
223            style: spec_api_style,
224            base_url: spec_base_url,
225            header_strategy: spec_header_strategy,
226            timeout: None,
227            retries: None,
228            retry_delay: None,
229            headers: None,
230        },
231        hooks: hooks_config,
232        modules: crate::config::model::ModulesConfig {
233            ignore: vec![],
234            selected: vec![],
235        },
236    };
237
238    // Add spec to config
239    config.specs.push(new_spec.clone());
240
241    // Validate updated config
242    validate_config(&config)?;
243
244    // Save config
245    save_config(&config)?;
246    println!("{}", "✅ Added spec to .vika.json".green());
247    println!();
248
249    // Create directory structure
250    let schemas_dir = PathBuf::from(&new_spec.schemas.output);
251    ensure_directory(&schemas_dir)?;
252
253    let apis_dir = PathBuf::from(&new_spec.apis.output);
254    ensure_directory(&apis_dir)?;
255
256    // Write runtime client at root_dir (shared across all specs)
257    let root_dir_path = PathBuf::from(&config.root_dir);
258    ensure_directory(&root_dir_path)?;
259    let runtime_dir = root_dir_path.join("runtime");
260    if !runtime_dir.exists() {
261        write_runtime_client(&root_dir_path, None, Some(&new_spec.apis))?;
262        println!(
263            "{}",
264            format!("✅ Created runtime client for spec '{}'", new_spec.name).green()
265        );
266    } else {
267        println!(
268            "{}",
269            format!(
270                "⚠️  Runtime client already exists for spec '{}'. Skipping.",
271                new_spec.name
272            )
273            .yellow()
274        );
275    }
276
277    println!();
278    println!("{}", "✨ Spec added successfully!".bright_green());
279    println!();
280
281    // Ask if user wants to generate code now
282    let generate_now = Confirm::new()
283        .with_prompt("Generate code for this spec now?")
284        .default(true)
285        .interact()
286        .map_err(|e| GenerationError::InvalidOperation {
287            message: format!("Failed to get user input: {}", e),
288        })?;
289
290    if generate_now {
291        println!();
292        println!("{}", "🚀 Starting code generation...".bright_cyan());
293        println!();
294
295        // Call generate command for this specific spec
296        use crate::commands::generate;
297        if let Err(e) = generate::run(
298            None,                                           // spec - will use spec_name
299            false,                                          // all_specs
300            Some(new_spec.name.clone()),                    // spec_name
301            false,                                          // verbose
302            config.generation.enable_cache,                 // cache
303            config.generation.enable_backup,                // backup
304            config.generation.conflict_strategy == "force", // force
305            false,                                          // react_query
306            false,                                          // swr
307        )
308        .await
309        {
310            println!();
311            println!(
312                "{}",
313                "⚠️  Generation failed, but spec was added successfully.".yellow()
314            );
315            println!("{}", format!("Error: {}", e).red());
316            println!();
317            println!(
318                "You can run 'vika-cli generate --spec-name {}' manually to retry.",
319                new_spec.name
320            );
321            return Ok(()); // Don't fail add if generation fails
322        }
323
324        println!();
325        println!("{}", "✅ Spec added and code generated!".bright_green());
326    } else {
327        println!("Next steps:");
328        println!("  Run: vika-cli generate --spec-name {}", new_spec.name);
329    }
330
331    Ok(())
332}