use anyhow::Result;
use std::fs;
use std::path::Path;
pub fn run(name: &str, path: Option<String>) -> Result<()> {
let project_dir = path.unwrap_or_else(|| name.to_string());
let project_path = Path::new(&project_dir);
println!("Creating new rustbridge plugin: {}", name);
println!("Directory: {}", project_dir);
if project_path.exists() {
anyhow::bail!("Directory already exists: {}", project_dir);
}
fs::create_dir_all(project_path.join("src"))?;
fs::create_dir_all(project_path.join("schemas"))?;
let cargo_toml = format!(
r#"[package]
name = "{name}"
version = "0.6.0"
edition = "2021"
[workspace] # Standalone project (not part of a parent workspace)
[lib]
crate-type = ["cdylib"]
[dependencies]
rustbridge = "0.6"
# Serde is needed as a direct dependency because derive macros expand to
# code that references ::serde:: directly
serde = {{ version = "1.0", features = ["derive"] }}
"#,
name = name,
);
fs::write(project_path.join("Cargo.toml"), cargo_toml)?;
let manifest = format!(
r#"[plugin]
name = "{name}"
version = "0.6.0"
description = "A rustbridge plugin"
[messages."echo"]
description = "Echo the input"
[platforms]
linux-x86_64 = "lib{name}.so"
darwin-aarch64 = "lib{name}.dylib"
darwin-x86_64 = "lib{name}.dylib"
windows-x86_64 = "{name}.dll"
"#,
name = name,
);
fs::write(project_path.join("rustbridge.toml"), manifest)?;
let lib_rs = format!(
r#"//! {name} - A rustbridge plugin
use rustbridge::prelude::*;
/// Echo request message
#[derive(Debug, Clone, Serialize, Deserialize, Message)]
#[message(tag = "echo")]
pub struct EchoRequest {{
pub message: String,
}}
/// Echo response message
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EchoResponse {{
pub message: String,
pub length: usize,
}}
/// Plugin implementation
#[derive(Default)]
pub struct {class_name};
#[async_trait]
impl Plugin for {class_name} {{
async fn on_start(&self, _ctx: &PluginContext) -> PluginResult<()> {{
rustbridge::tracing::info!("{name} plugin started");
Ok(())
}}
async fn handle_request(
&self,
_ctx: &PluginContext,
type_tag: &str,
payload: &[u8],
) -> PluginResult<Vec<u8>> {{
match type_tag {{
"echo" => {{
let req: EchoRequest = rustbridge::serde_json::from_slice(payload)?;
let response = EchoResponse {{
length: req.message.len(),
message: req.message,
}};
Ok(rustbridge::serde_json::to_vec(&response)?)
}}
_ => Err(PluginError::UnknownMessageType(type_tag.to_string())),
}}
}}
async fn on_stop(&self, _ctx: &PluginContext) -> PluginResult<()> {{
rustbridge::tracing::info!("{name} plugin stopped");
Ok(())
}}
fn supported_types(&self) -> Vec<&'static str> {{
vec!["echo"]
}}
}}
// Generate FFI entry point
rustbridge_entry!({class_name}::default);
// Re-export FFI functions for the shared library
pub use rustbridge::ffi_exports::*;
#[cfg(test)]
mod tests {{
use super::*;
#[rustbridge::tokio::test]
async fn test_echo() {{
let plugin = {class_name};
let ctx = PluginContext::new(PluginConfig::default());
let request = rustbridge::serde_json::to_vec(&EchoRequest {{
message: "Hello, World!".to_string(),
}})
.unwrap();
let response = plugin.handle_request(&ctx, "echo", &request).await.unwrap();
let echo_response: EchoResponse = rustbridge::serde_json::from_slice(&response).unwrap();
assert_eq!(echo_response.message, "Hello, World!");
assert_eq!(echo_response.length, 13);
}}
}}
"#,
name = name,
class_name = to_pascal_case(name),
);
fs::write(project_path.join("src/lib.rs"), lib_rs)?;
let gitignore = r#"/target/
Cargo.lock
*.swp
*.swo
.idea/
.vscode/
"#;
fs::write(project_path.join(".gitignore"), gitignore)?;
println!("\n✓ Project created successfully!");
println!("\nNext steps:");
println!(" cd {}", project_dir);
println!(" cargo build");
println!(" rustbridge check");
Ok(())
}
fn to_pascal_case(s: &str) -> String {
s.split(['-', '_'])
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().chain(chars).collect(),
}
})
.collect()
}