use serde::Serialize; use std::path::PathBuf; use crate::app_error::AppCommandError; // --------------------------------------------------------------------------- // Package manager detection // --------------------------------------------------------------------------- #[derive(Debug, Clone, Serialize)] pub struct PackageManagerInfo { pub name: String, pub installed: bool, pub version: Option, } async fn detect_one(name: &str) -> PackageManagerInfo { let program = match name { "bun" => "bun", "pnpm" => "pnpm", "yarn" => "yarn", _ => "npm", }; let result = crate::process::tokio_command(program) .arg("--version") .output() .await; match result { Ok(output) if output.status.success() => { let version = String::from_utf8_lossy(&output.stdout) .trim() .to_string(); PackageManagerInfo { name: name.to_string(), installed: true, version: Some(version), } } _ => PackageManagerInfo { name: name.to_string(), installed: false, version: None, }, } } #[tauri::command] pub async fn detect_package_manager(name: String) -> PackageManagerInfo { detect_one(&name).await } // --------------------------------------------------------------------------- // Project creation // --------------------------------------------------------------------------- #[tauri::command] pub async fn create_shadcn_project( project_name: String, template: String, preset_code: String, package_manager: String, target_dir: String, ) -> Result { let project_name = project_name.trim().to_string(); let template = template.trim().to_string(); let preset_code = preset_code.trim().to_string(); let package_manager = package_manager.trim().to_string(); let target_dir = target_dir.trim().to_string(); if project_name.is_empty() { return Err(AppCommandError::invalid_input("Project name is required")); } if template.is_empty() { return Err(AppCommandError::invalid_input("Template is required")); } if target_dir.is_empty() { return Err(AppCommandError::invalid_input("Target directory is required")); } let full_path = PathBuf::from(&target_dir).join(&project_name); let full_path_str = full_path.to_string_lossy().to_string(); // Check if directory already exists and is non-empty if full_path.exists() { let is_empty = full_path .read_dir() .map(|mut entries| entries.next().is_none()) .unwrap_or(false); if !is_empty { return Err(AppCommandError::already_exists( "Target directory already exists and is not empty", )); } } // Determine the command based on package manager let (program, prefix_args): (&str, Vec<&str>) = match package_manager.as_str() { "pnpm" => ("pnpm", vec!["dlx"]), "yarn" => ("yarn", vec!["dlx"]), "bun" => ("bunx", vec![]), _ => ("npx", vec![]), }; let mut cmd = crate::process::tokio_command(program); cmd.args(&prefix_args); cmd.args([ "shadcn@latest", "init", "-n", &project_name, "-t", &template, "-p", &preset_code, "-y", ]); cmd.current_dir(&target_dir); // Log the full command for debugging let cmd_display = format!( "{} {} shadcn@latest init -n {} -t {} -p {} -y (cwd={})", program, prefix_args.join(" "), project_name, template, preset_code, target_dir ); eprintln!("[ProjectBoot] executing: {cmd_display}"); let output = cmd.output().await.map_err(|e| { eprintln!("[ProjectBoot] spawn error: {e}"); if e.kind() == std::io::ErrorKind::NotFound { AppCommandError::dependency_missing(format!( "{program} is not installed. Please install Node.js first." )) } else { AppCommandError::external_command( "Failed to execute project creation command", e.to_string(), ) } })?; let stdout = String::from_utf8_lossy(&output.stdout).to_string(); let stderr = String::from_utf8_lossy(&output.stderr).to_string(); eprintln!( "[ProjectBoot] exit={} stdout_len={} stderr_len={}", output.status, stdout.len(), stderr.len() ); if !stdout.is_empty() { eprintln!("[ProjectBoot] stdout: {stdout}"); } if !stderr.is_empty() { eprintln!("[ProjectBoot] stderr: {stderr}"); } if !output.status.success() { let mut detail = String::new(); if !stderr.is_empty() { detail.push_str(&stderr); } if !stdout.is_empty() { if !detail.is_empty() { detail.push('\n'); } detail.push_str(&stdout); } if detail.is_empty() { detail = format!("Command exited with status: {}", output.status); } return Err(AppCommandError::external_command( "Project creation command failed", detail, )); } Ok(full_path_str) }