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 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 let mut config = load_config()?;
24 validate_config(&config)?;
25
26 println!("{}", "Let's configure your new spec:".bright_cyan());
27 println!();
28
29 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 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 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 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 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 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 config.specs.push(new_spec.clone());
240
241 validate_config(&config)?;
243
244 save_config(&config)?;
246 println!("{}", "✅ Added spec to .vika.json".green());
247 println!();
248
249 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 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 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 use crate::commands::generate;
297 if let Err(e) = generate::run(
298 None, false, Some(new_spec.name.clone()), false, config.generation.enable_cache, config.generation.enable_backup, config.generation.conflict_strategy == "force", false, false, )
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(()); }
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}