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_http_client_template};
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        // Create config with user preferences
215        let config = Config {
216            root_dir,
217            generation: crate::config::model::GenerationConfig {
218                enable_cache,
219                enable_backup,
220                conflict_strategy,
221            },
222            specs: vec![crate::config::model::SpecEntry {
223                name: spec_name.trim().to_string(),
224                path: spec_path_input.trim().to_string(),
225                schemas: crate::config::model::SchemasConfig {
226                    output: spec_schemas_output.trim().to_string(),
227                    naming: spec_naming,
228                },
229                apis: crate::config::model::ApisConfig {
230                    output: spec_apis_output.trim().to_string(),
231                    style: spec_api_style,
232                    base_url: spec_base_url,
233                    header_strategy: spec_header_strategy,
234                },
235                modules: crate::config::model::ModulesConfig {
236                    ignore: vec![],
237                    selected: vec![],
238                },
239            }],
240            ..Config::default()
241        };
242
243        validate_config(&config)?;
244        save_config(&config)?;
245        println!("{}", "✅ Created .vika.json".green());
246    }
247
248    // Create directory structure
249    let config = crate::config::loader::load_config()?;
250
251    let root_dir = PathBuf::from(&config.root_dir);
252    ensure_directory(&root_dir)?;
253
254    // Create directories for each spec
255    for spec in &config.specs {
256        let schemas_dir = PathBuf::from(&spec.schemas.output);
257        ensure_directory(&schemas_dir)?;
258
259        let apis_dir = PathBuf::from(&spec.apis.output);
260        ensure_directory(&apis_dir)?;
261
262        // Write http client template for each spec
263        let http_client_path = apis_dir.join("http.ts");
264        if !http_client_path.exists() {
265            write_http_client_template(&http_client_path)?;
266            println!(
267                "{}",
268                format!(
269                    "✅ Created {} for spec '{}'",
270                    http_client_path.display(),
271                    spec.name
272                )
273                .green()
274            );
275        } else {
276            println!(
277                "{}",
278                format!(
279                    "⚠️  {} already exists for spec '{}'. Skipping.",
280                    http_client_path.display(),
281                    spec.name
282                )
283                .yellow()
284            );
285        }
286    }
287
288    println!();
289    println!("{}", "✨ Project initialized successfully!".bright_green());
290    println!();
291
292    // Automatically trigger generation if specs are provided
293    if !config.specs.is_empty() {
294        println!("{}", "🚀 Starting code generation...".bright_cyan());
295        println!();
296
297        // Call generate command internally
298        use crate::commands::generate;
299        if let Err(e) = generate::run(
300            None,                                           // spec - will use config.specs
301            false,                                          // all_specs
302            None,                                           // spec_name
303            false,                                          // verbose
304            config.generation.enable_cache,                 // cache
305            config.generation.enable_backup,                // backup
306            config.generation.conflict_strategy == "force", // force
307        )
308        .await
309        {
310            println!();
311            println!(
312                "{}",
313                "⚠️  Generation failed, but initialization completed.".yellow()
314            );
315            println!("{}", format!("Error: {}", e).red());
316            println!();
317            println!("You can run 'vika-cli generate' manually to retry.");
318            return Ok(()); // Don't fail init if generation fails
319        }
320
321        println!();
322        println!(
323            "{}",
324            "✅ Initialization and generation completed!".bright_green()
325        );
326        println!();
327        println!("💡 To add more specs later, run: vika-cli add");
328    } else {
329        println!("Next steps:");
330        println!("  1. Run: vika-cli generate");
331        println!("  2. Select the modules you want to generate");
332        println!();
333        println!("💡 To add more specs later, run: vika-cli add");
334    }
335
336    Ok(())
337}