use anyhow::{Result, bail}; use colored::*; use std::path::PathBuf; use crate::templates::{TemplateManager, TemplateContext}; use crate::version::ANVIL_VERSION; pub fn list_templates() -> Result<()> { println!("{}", "Available templates:".bright_cyan().bold()); println!(); for info in TemplateManager::list_templates() { let marker = if info.is_default { " (default)" } else { "" }; println!( " {}{}", info.name.bright_white().bold(), marker.bright_cyan() ); println!(" {}", info.description); println!(); } println!("{}", "Usage:".bright_yellow().bold()); println!(" anvil new "); println!(" anvil new --template basic"); println!(); Ok(()) } pub fn create_project(name: &str, template: Option<&str>) -> Result<()> { // Validate project name validate_project_name(name)?; let project_path = PathBuf::from(name); if project_path.exists() { bail!( "Directory already exists: {}\n\ Choose a different name or remove the existing directory.", project_path.display() ); } let template_name = template.unwrap_or("basic"); if !TemplateManager::template_exists(template_name) { println!( "{}", format!("Template '{}' not found.", template_name).red().bold() ); println!(); list_templates()?; bail!("Invalid template"); } println!( "{}", format!("Creating Arduino project: {}", name) .bright_green() .bold() ); println!("{}", format!("Template: {}", template_name).bright_cyan()); println!(); // Create project directory std::fs::create_dir_all(&project_path)?; // Extract template println!("{}", "Extracting template files...".bright_yellow()); let context = TemplateContext { project_name: name.to_string(), anvil_version: ANVIL_VERSION.to_string(), }; let file_count = TemplateManager::extract(template_name, &project_path, &context)?; println!("{} Extracted {} files", "ok".green(), file_count); // Make shell scripts executable on Unix #[cfg(unix)] { make_executable(&project_path); } // Initialize git repository init_git(&project_path, template_name); // Print success println!(); println!( "{}", "================================================================" .bright_green() ); println!( "{}", format!(" Project created: {}", name) .bright_green() .bold() ); println!( "{}", "================================================================" .bright_green() ); println!(); print_next_steps(name); Ok(()) } fn validate_project_name(name: &str) -> Result<()> { if name.is_empty() { bail!("Project name cannot be empty"); } if name.len() > 50 { bail!("Project name must be 50 characters or less"); } if !name.chars().next().unwrap().is_alphabetic() { bail!("Project name must start with a letter"); } let valid = name .chars() .all(|c| c.is_alphanumeric() || c == '-' || c == '_'); if !valid { bail!( "Invalid project name '{}'\n\ Names may contain: letters, numbers, hyphens, underscores\n\ Must start with a letter.\n\n\ Valid examples:\n\ \x20 blink\n\ \x20 my-sensor\n\ \x20 team1234_robot", name ); } Ok(()) } fn init_git(project_dir: &PathBuf, template_name: &str) { println!("{}", "Initializing git repository...".bright_yellow()); // Check if git is available if which::which("git").is_err() { eprintln!( "{} git not found, skipping repository initialization.", "warn".yellow() ); return; } let run = |args: &[&str]| -> bool { std::process::Command::new("git") .args(args) .current_dir(project_dir) .output() .map(|o| o.status.success()) .unwrap_or(false) }; if !run(&["init"]) { eprintln!("{} Failed to initialize git.", "warn".yellow()); return; } run(&["add", "."]); let msg = format!( "Initial commit from anvil new --template {}", template_name ); run(&["commit", "-m", &msg]); println!("{} Git repository initialized", "ok".green()); } #[cfg(unix)] fn make_executable(project_dir: &PathBuf) { use std::os::unix::fs::PermissionsExt; let scripts = [ "build.sh", "upload.sh", "monitor.sh", "test/run_tests.sh", ]; for script in &scripts { let path = project_dir.join(script); if path.exists() { if let Ok(meta) = std::fs::metadata(&path) { let mut perms = meta.permissions(); perms.set_mode(0o755); let _ = std::fs::set_permissions(&path, perms); } } } } fn print_next_steps(project_name: &str) { println!("{}", "Next steps:".bright_yellow().bold()); println!( " 1. {}", format!("cd {}", project_name).bright_cyan() ); if cfg!(target_os = "windows") { println!( " 2. Compile: {}", "build.bat".bright_cyan() ); println!( " 3. Upload to board: {}", "upload.bat".bright_cyan() ); println!( " 4. Upload + monitor: {}", "upload.bat --monitor".bright_cyan() ); println!( " 5. Serial monitor: {}", "monitor.bat".bright_cyan() ); println!( " 6. Run host tests: {}", "test\\run_tests.bat".bright_cyan() ); println!(); println!( " {}", "On Linux/macOS: ./build.sh, ./upload.sh, ./monitor.sh" .bright_black() ); } else { println!( " 2. Compile: {}", "./build.sh".bright_cyan() ); println!( " 3. Upload to board: {}", "./upload.sh".bright_cyan() ); println!( " 4. Upload + monitor: {}", "./upload.sh --monitor".bright_cyan() ); println!( " 5. Serial monitor: {}", "./monitor.sh".bright_cyan() ); println!( " 6. Run host tests: {}", "./test/run_tests.sh".bright_cyan() ); println!(); println!( " {}", "On Windows: build.bat, upload.bat, monitor.bat, test\\run_tests.bat" .bright_black() ); } println!( " {}", "System check: anvil doctor | Port scan: anvil devices" .bright_black() ); println!(); } #[cfg(test)] mod tests { use super::*; #[test] fn test_validate_name_valid() { assert!(validate_project_name("blink").is_ok()); assert!(validate_project_name("my-sensor").is_ok()); assert!(validate_project_name("team1234_robot").is_ok()); } #[test] fn test_validate_name_empty() { assert!(validate_project_name("").is_err()); } #[test] fn test_validate_name_starts_with_number() { assert!(validate_project_name("123abc").is_err()); } #[test] fn test_validate_name_special_chars() { assert!(validate_project_name("my project").is_err()); assert!(validate_project_name("my.project").is_err()); } #[test] fn test_validate_name_too_long() { let long_name = "a".repeat(51); assert!(validate_project_name(&long_name).is_err()); } }