use crate::config::loader::{load_config, save_config};
use crate::config::validator::validate_config;
use crate::error::{GenerationError, Result};
use crate::generator::writer::{ensure_directory, write_runtime_client};
use colored::*;
use dialoguer::{Confirm, Input, Select};
use std::path::PathBuf;
pub async fn run() -> Result<()> {
println!("{}", "Adding new spec to vika-cli project...".bright_cyan());
println!();
let config_path = PathBuf::from(".vika.json");
if !config_path.exists() {
return Err(GenerationError::InvalidOperation {
message: ".vika.json not found. Please run 'vika-cli init' first.".to_string(),
}
.into());
}
let mut config = load_config()?;
validate_config(&config)?;
println!("{}", "Let's configure your new spec:".bright_cyan());
println!();
let spec_name: String = Input::new()
.with_prompt("Spec name (kebab-case recommended, e.g., 'ecommerce', 'auth-api')")
.validate_with(|input: &String| -> std::result::Result<(), String> {
if input.trim().is_empty() {
return Err("Spec name cannot be empty".to_string());
}
if config.specs.iter().any(|s| s.name == input.trim()) {
return Err(format!("Spec '{}' already exists", input.trim()));
}
Ok(())
})
.interact_text()
.map_err(|e| GenerationError::InvalidOperation {
message: format!("Failed to get user input: {}", e),
})?;
let spec_path_input: String = Input::new()
.with_prompt(format!("Path or URL for '{}'", spec_name.trim()))
.interact_text()
.map_err(|e| GenerationError::InvalidOperation {
message: format!("Failed to get user input: {}", e),
})?;
println!();
println!(
"{}",
format!("📋 Configuration for spec '{}'", spec_name.trim()).bright_cyan()
);
println!();
println!("{}", "📁 Schemas Configuration".bright_yellow());
println!();
let spec_schemas_output: String = Input::new()
.with_prompt(format!(
"Schemas output directory for '{}'",
spec_name.trim()
))
.default(format!("src/schemas/{}", spec_name.trim()))
.interact_text()
.map_err(|e| GenerationError::InvalidOperation {
message: format!("Failed to get user input: {}", e),
})?;
println!();
let spec_naming_options = ["PascalCase", "camelCase", "snake_case", "kebab-case"];
let spec_naming_index = Select::new()
.with_prompt(format!(
"Schema naming convention for '{}'",
spec_name.trim()
))
.items(&[
"PascalCase - ProductDto, UserProfile (recommended)",
"camelCase - productDto, userProfile",
"snake_case - product_dto, user_profile",
"kebab-case - product-dto, user-profile",
])
.default(0)
.interact()
.map_err(|e| GenerationError::InvalidOperation {
message: format!("Failed to get user selection: {}", e),
})?;
let spec_naming = spec_naming_options[spec_naming_index].to_string();
println!();
println!("{}", "🔌 API Configuration".bright_yellow());
println!();
let spec_apis_output: String = Input::new()
.with_prompt(format!("APIs output directory for '{}'", spec_name.trim()))
.default(format!("src/apis/{}", spec_name.trim()))
.interact_text()
.map_err(|e| GenerationError::InvalidOperation {
message: format!("Failed to get user input: {}", e),
})?;
println!();
let spec_api_style_options = ["fetch"];
let spec_api_style_index = Select::new()
.with_prompt(format!("API client style for '{}'", spec_name.trim()))
.items(&["fetch - Native Fetch API (recommended)"])
.default(0)
.interact()
.map_err(|e| GenerationError::InvalidOperation {
message: format!("Failed to get user selection: {}", e),
})?;
let spec_api_style = spec_api_style_options[spec_api_style_index].to_string();
println!();
let spec_base_url_input: String = Input::new()
.with_prompt(format!(
"API base URL for '{}' (optional, press Enter to skip)",
spec_name.trim()
))
.allow_empty(true)
.interact_text()
.map_err(|e| GenerationError::InvalidOperation {
message: format!("Failed to get user input: {}", e),
})?;
let spec_base_url = if spec_base_url_input.trim().is_empty() {
None
} else {
Some(spec_base_url_input.trim().to_string())
};
println!();
let spec_header_strategy_options = ["consumerInjected", "bearerToken", "fixed"];
let spec_header_strategy_index = Select::new()
.with_prompt(format!("Header strategy for '{}'", spec_name.trim()))
.items(&[
"consumerInjected - Headers provided by consumer (recommended)",
"bearerToken - Automatic Bearer token injection",
"fixed - Fixed headers from config",
])
.default(0)
.interact()
.map_err(|e| GenerationError::InvalidOperation {
message: format!("Failed to get user selection: {}", e),
})?;
let spec_header_strategy = spec_header_strategy_options[spec_header_strategy_index].to_string();
println!();
println!("{}", "📎 Hooks Configuration".bright_yellow());
println!();
let enable_hooks = Confirm::new()
.with_prompt("Do you want to generate hooks (React Query or SWR)?")
.default(false)
.interact()
.map_err(|e| GenerationError::InvalidOperation {
message: format!("Failed to get user input: {}", e),
})?;
let hooks_config = if enable_hooks {
let hook_library_options = ["react-query", "swr"];
let hook_library_index = Select::new()
.with_prompt("Which hook library do you want to use?")
.items(&hook_library_options)
.default(0)
.interact()
.map_err(|e| GenerationError::InvalidOperation {
message: format!("Failed to get user selection: {}", e),
})?;
let hook_library = hook_library_options[hook_library_index].to_string();
let hooks_output: String = Input::new()
.with_prompt("Hooks output directory")
.default("src/hooks".to_string())
.interact_text()
.map_err(|e| GenerationError::InvalidOperation {
message: format!("Failed to get user input: {}", e),
})?;
let query_keys_output: String = Input::new()
.with_prompt("Query keys output directory")
.default("src/query-keys".to_string())
.interact_text()
.map_err(|e| GenerationError::InvalidOperation {
message: format!("Failed to get user input: {}", e),
})?;
Some(crate::config::model::HooksConfig {
output: hooks_output.trim().to_string(),
query_keys_output: query_keys_output.trim().to_string(),
library: Some(hook_library),
})
} else {
None
};
println!();
let new_spec = crate::config::model::SpecEntry {
name: spec_name.trim().to_string(),
path: spec_path_input.trim().to_string(),
schemas: crate::config::model::SchemasConfig {
output: spec_schemas_output.trim().to_string(),
naming: spec_naming,
},
apis: crate::config::model::ApisConfig {
output: spec_apis_output.trim().to_string(),
style: spec_api_style,
base_url: spec_base_url,
header_strategy: spec_header_strategy,
timeout: None,
retries: None,
retry_delay: None,
headers: None,
},
hooks: hooks_config,
modules: crate::config::model::ModulesConfig {
ignore: vec![],
selected: vec![],
},
};
config.specs.push(new_spec.clone());
validate_config(&config)?;
save_config(&config)?;
println!("{}", "✅ Added spec to .vika.json".green());
println!();
let schemas_dir = PathBuf::from(&new_spec.schemas.output);
ensure_directory(&schemas_dir)?;
let apis_dir = PathBuf::from(&new_spec.apis.output);
ensure_directory(&apis_dir)?;
let root_dir_path = PathBuf::from(&config.root_dir);
ensure_directory(&root_dir_path)?;
let runtime_dir = root_dir_path.join("runtime");
if !runtime_dir.exists() {
write_runtime_client(&root_dir_path, None, Some(&new_spec.apis))?;
println!(
"{}",
format!("✅ Created runtime client for spec '{}'", new_spec.name).green()
);
} else {
println!(
"{}",
format!(
"⚠️ Runtime client already exists for spec '{}'. Skipping.",
new_spec.name
)
.yellow()
);
}
println!();
println!("{}", "✨ Spec added successfully!".bright_green());
println!();
let generate_now = Confirm::new()
.with_prompt("Generate code for this spec now?")
.default(true)
.interact()
.map_err(|e| GenerationError::InvalidOperation {
message: format!("Failed to get user input: {}", e),
})?;
if generate_now {
println!();
println!("{}", "🚀 Starting code generation...".bright_cyan());
println!();
use crate::commands::generate;
if let Err(e) = generate::run(
None, false, Some(new_spec.name.clone()), false, config.generation.enable_cache, config.generation.enable_backup, config.generation.conflict_strategy == "force", false, false, )
.await
{
println!();
println!(
"{}",
"⚠️ Generation failed, but spec was added successfully.".yellow()
);
println!("{}", format!("Error: {}", e).red());
println!();
println!(
"You can run 'vika-cli generate --spec-name {}' manually to retry.",
new_spec.name
);
return Ok(()); }
println!();
println!("{}", "✅ Spec added and code generated!".bright_green());
} else {
println!("Next steps:");
println!(" Run: vika-cli generate --spec-name {}", new_spec.name);
}
Ok(())
}