vika_cli/commands/
init.rs

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