185 lines
5.3 KiB
Rust
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)
|
|
}
|