use crate::templates::{generate_project, PluginType, TemplateData};
use crate::utils::{ensure_dir, to_kebab_case};
use anyhow::{Context, Result};
use colored::*;
use std::path::Path;
use std::process::Command;
pub async fn create_plugin_project(
name: &str,
plugin_type_str: &str,
output: Option<&Path>,
author_name: Option<&str>,
author_email: Option<&str>,
init_git: bool,
) -> Result<()> {
let plugin_type = PluginType::from_str(plugin_type_str)?;
let plugin_name = Path::new(name).file_name().and_then(|n| n.to_str()).unwrap_or(name);
let plugin_id = to_kebab_case(plugin_name);
let output_dir = if name.contains('/') || name.contains('\\') {
Path::new(name).to_path_buf()
} else if let Some(out) = output {
out.join(&plugin_id)
} else {
std::env::current_dir()?.join(&plugin_id)
};
if output_dir.exists() {
anyhow::bail!(
"Directory {} already exists. Choose a different name or location.",
output_dir.display()
);
}
println!("{}", "Creating new plugin project...".cyan().bold());
println!(" {} {}", "Name:".bold(), plugin_name);
println!(" {} {}", "Type:".bold(), plugin_type.as_str());
println!(" {} {}", "Directory:".bold(), output_dir.display());
println!();
ensure_dir(&output_dir)?;
let template_data = TemplateData {
plugin_name: plugin_name.to_string(),
plugin_id: plugin_id.clone(),
plugin_type,
author_name: author_name.map(String::from),
author_email: author_email.map(String::from),
};
generate_project(&template_data, &output_dir)
.context("Failed to generate project from template")?;
println!("{}", "✓ Project files generated".green());
if init_git {
init_git_repo(&output_dir)?;
println!("{}", "✓ Git repository initialized".green());
}
println!();
println!("{}", "Next steps:".bold().green());
println!(" 1. cd {}", plugin_id);
println!(" 2. cargo build --target wasm32-wasi --release");
println!(" 3. cargo test");
println!();
println!("{}", "Or use the MockForge plugin CLI:".bold());
println!(" mockforge-plugin build --release");
println!(" mockforge-plugin test");
println!(" mockforge-plugin package");
Ok(())
}
fn init_git_repo(dir: &Path) -> Result<()> {
let status = Command::new("git")
.arg("init")
.current_dir(dir)
.status()
.context("Failed to execute git init. Is git installed?")?;
if !status.success() {
anyhow::bail!("Git init failed");
}
let _ = Command::new("git").args(["add", "."]).current_dir(dir).status();
let _ = Command::new("git")
.args(["commit", "-m", "Initial commit"])
.current_dir(dir)
.status();
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
use tokio::sync::Mutex;
static CWD_MUTEX: Mutex<()> = Mutex::const_new(());
#[tokio::test]
async fn test_create_plugin_project_basic() {
let temp_dir = TempDir::new().unwrap();
let result =
create_plugin_project("test-plugin", "auth", Some(temp_dir.path()), None, None, false)
.await;
assert!(result.is_ok());
let plugin_dir = temp_dir.path().join("test-plugin");
assert!(plugin_dir.exists());
assert!(plugin_dir.join("Cargo.toml").exists());
assert!(plugin_dir.join("plugin.yaml").exists());
assert!(plugin_dir.join("src/lib.rs").exists());
assert!(plugin_dir.join("README.md").exists());
assert!(plugin_dir.join(".gitignore").exists());
}
#[tokio::test]
async fn test_create_plugin_project_with_author() {
let temp_dir = TempDir::new().unwrap();
let result = create_plugin_project(
"author-plugin",
"template",
Some(temp_dir.path()),
Some("John Doe"),
Some("john@example.com"),
false,
)
.await;
assert!(result.is_ok());
let plugin_dir = temp_dir.path().join("author-plugin");
let cargo_content = fs::read_to_string(plugin_dir.join("Cargo.toml")).unwrap();
assert!(cargo_content.contains("John Doe"));
assert!(cargo_content.contains("john@example.com"));
let manifest_content = fs::read_to_string(plugin_dir.join("plugin.yaml")).unwrap();
assert!(manifest_content.contains("John Doe"));
assert!(manifest_content.contains("john@example.com"));
}
#[tokio::test]
async fn test_create_plugin_project_all_types() {
let temp_dir = TempDir::new().unwrap();
for plugin_type in &["auth", "template", "response", "datasource"] {
let result = create_plugin_project(
&format!("{}-plugin", plugin_type),
plugin_type,
Some(temp_dir.path()),
None,
None,
false,
)
.await;
assert!(result.is_ok(), "Failed for plugin type: {}", plugin_type);
let plugin_dir = temp_dir.path().join(format!("{}-plugin", plugin_type));
assert!(plugin_dir.exists());
let manifest_content = fs::read_to_string(plugin_dir.join("plugin.yaml")).unwrap();
assert!(manifest_content.contains(&format!("plugin_type: {}", plugin_type)));
}
}
#[tokio::test]
async fn test_create_plugin_project_invalid_type() {
let temp_dir = TempDir::new().unwrap();
let result = create_plugin_project(
"bad-plugin",
"invalid-type",
Some(temp_dir.path()),
None,
None,
false,
)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_create_plugin_project_already_exists() {
let temp_dir = TempDir::new().unwrap();
create_plugin_project("existing", "auth", Some(temp_dir.path()), None, None, false)
.await
.unwrap();
let result =
create_plugin_project("existing", "auth", Some(temp_dir.path()), None, None, false)
.await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("already exists"));
}
#[tokio::test]
async fn test_create_plugin_project_kebab_case_conversion() {
let temp_dir = TempDir::new().unwrap();
let result = create_plugin_project(
"My Plugin Name",
"auth",
Some(temp_dir.path()),
None,
None,
false,
)
.await;
assert!(result.is_ok());
let plugin_dir = temp_dir.path().join("my-plugin-name");
assert!(plugin_dir.exists());
let manifest_content = fs::read_to_string(plugin_dir.join("plugin.yaml")).unwrap();
assert!(manifest_content.contains("id: my-plugin-name"));
}
#[tokio::test]
async fn test_create_plugin_project_with_path_in_name() {
let temp_dir = TempDir::new().unwrap();
let project_path = temp_dir.path().join("custom/path/plugin");
let result =
create_plugin_project(project_path.to_str().unwrap(), "auth", None, None, None, false)
.await;
assert!(result.is_ok());
assert!(project_path.exists());
assert!(project_path.join("Cargo.toml").exists());
}
#[tokio::test]
async fn test_create_plugin_project_default_output() {
let _guard = CWD_MUTEX.lock().await;
let temp_dir = TempDir::new().unwrap();
let original_dir = std::env::current_dir().unwrap();
std::env::set_current_dir(&temp_dir).unwrap();
let result = create_plugin_project("default-out", "auth", None, None, None, false).await;
std::env::set_current_dir(&original_dir).unwrap();
assert!(result.is_ok());
let plugin_dir = temp_dir.path().join("default-out");
assert!(plugin_dir.exists());
}
#[tokio::test]
async fn test_create_plugin_project_files_content() {
let temp_dir = TempDir::new().unwrap();
create_plugin_project("content-test", "auth", Some(temp_dir.path()), None, None, false)
.await
.unwrap();
let plugin_dir = temp_dir.path().join("content-test");
let cargo_content = fs::read_to_string(plugin_dir.join("Cargo.toml")).unwrap();
assert!(cargo_content.contains("name = \"content-test\""));
assert!(cargo_content.contains("crate-type = [\"cdylib\"]"));
assert!(cargo_content.contains("mockforge-plugin-sdk"));
let manifest_content = fs::read_to_string(plugin_dir.join("plugin.yaml")).unwrap();
assert!(manifest_content.contains("id: content-test"));
assert!(manifest_content.contains("plugin_type: auth"));
assert!(manifest_content.contains("capabilities:"));
let gitignore_content = fs::read_to_string(plugin_dir.join(".gitignore")).unwrap();
assert!(gitignore_content.contains("/target"));
assert!(gitignore_content.contains("*.wasm"));
let readme_content = fs::read_to_string(plugin_dir.join("README.md")).unwrap();
assert!(readme_content.contains("content-test"));
assert!(readme_content.contains("auth"));
assert!(plugin_dir.join("src/lib.rs").exists());
}
#[tokio::test]
async fn test_create_plugin_project_data_source_hyphen() {
let temp_dir = TempDir::new().unwrap();
let result = create_plugin_project(
"ds-plugin",
"data-source",
Some(temp_dir.path()),
None,
None,
false,
)
.await;
assert!(result.is_ok());
let plugin_dir = temp_dir.path().join("ds-plugin");
let manifest_content = fs::read_to_string(plugin_dir.join("plugin.yaml")).unwrap();
assert!(manifest_content.contains("plugin_type: datasource"));
}
#[test]
fn test_init_git_repo() {
let temp_dir = TempDir::new().unwrap();
fs::create_dir_all(temp_dir.path()).unwrap();
fs::write(temp_dir.path().join("test.txt"), "test").unwrap();
let result = init_git_repo(temp_dir.path());
match result {
Ok(_) => {
assert!(temp_dir.path().join(".git").exists());
}
Err(_) => {
}
}
}
#[tokio::test]
async fn test_create_plugin_project_no_git() {
let temp_dir = TempDir::new().unwrap();
let result = create_plugin_project(
"no-git",
"auth",
Some(temp_dir.path()),
None,
None,
false, )
.await;
assert!(result.is_ok());
let plugin_dir = temp_dir.path().join("no-git");
assert!(plugin_dir.exists());
}
}