Files
codeg/src-tauri/src/commands/project_boot.rs
2026-03-27 15:08:53 +08:00

185 lines
5.3 KiB
Rust

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<String>,
}
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<String, AppCommandError> {
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)
}