use std::path::{Path, PathBuf};
use std::process::Command;
use std::{env, fs};
use anyhow::Result;
use dialoguer::Input;
use semver::Version;
use surrealism_runtime::PrefixErr;
use surrealism_runtime::config::{AbiVersion, SurrealismConfig, SurrealismMeta, Target};
const SURREALISM_VERSION: &str = surrealism_runtime::SDK_VERSION;
const SURREALDB_VERSION: &str = env!("CARGO_PKG_VERSION");
pub async fn init(
path: Option<PathBuf>,
org: Option<String>,
name: Option<String>,
headless: bool,
) -> Result<()> {
let path = resolve_path(path)?;
validate_directory(&path)?;
let dir_name = dir_name_sanitized(&path);
let org = collect_org(org, headless)?;
let name = collect_name(name, dir_name, headless)?;
let target = Target::Rust;
match target {
Target::Rust => scaffold_rust(&path, &name)?,
}
write_surrealism_toml(&path, target, &org, &name)?;
println!();
println!("Surrealism module initialised at {}", path.display());
println!();
println!(" Organisation : {org}");
println!(" Module name : {name}");
println!(" Target : {target}");
println!();
println!("Next steps:");
println!(" cd {}", path.display());
println!(" surreal module build");
println!();
Ok(())
}
fn resolve_path(path: Option<PathBuf>) -> Result<PathBuf> {
match path {
Some(p) if p.is_absolute() => Ok(p),
Some(p) => {
Ok(env::current_dir().prefix_err(|| "Failed to determine current directory")?.join(p))
}
None => Ok(env::current_dir().prefix_err(|| "Failed to determine current directory")?),
}
}
fn validate_directory(path: &Path) -> Result<()> {
if !path.exists() {
fs::create_dir_all(path)
.prefix_err(|| format!("Failed to create directory: {}", path.display()))?;
return Ok(());
}
if !path.is_dir() {
anyhow::bail!("Path exists but is not a directory: {}", path.display());
}
let has_unexpected_entries = fs::read_dir(path)
.prefix_err(|| format!("Failed to read directory: {}", path.display()))?
.filter_map(|e| e.ok())
.any(|entry| {
let name = entry.file_name();
let name = name.to_string_lossy();
name != ".git" && name != ".gitignore"
});
if has_unexpected_entries {
anyhow::bail!(
"Directory is not empty: {}\n\
Run `surreal module init` in a new or empty directory.",
path.display()
);
}
Ok(())
}
fn dir_name_sanitized(path: &Path) -> String {
path.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "my-module".to_string())
.chars()
.map(|c| {
if c.is_alphanumeric() || c == '-' || c == '_' {
c
} else {
'-'
}
})
.collect()
}
fn collect_org(org: Option<String>, headless: bool) -> Result<String> {
if let Some(org) = org {
return Ok(org);
}
if headless {
anyhow::bail!("--org is required in headless mode");
}
let org: String = Input::new()
.with_prompt("Organisation name")
.interact_text()
.prefix_err(|| "Failed to read organisation name")?;
if org.trim().is_empty() {
anyhow::bail!("Organisation name cannot be empty");
}
Ok(org.trim().to_string())
}
fn collect_name(name: Option<String>, default: String, headless: bool) -> Result<String> {
if let Some(name) = name {
return Ok(name);
}
if headless {
return Ok(default);
}
let name: String = Input::new()
.with_prompt("Module name")
.default(default)
.interact_text()
.prefix_err(|| "Failed to read module name")?;
if name.trim().is_empty() {
anyhow::bail!("Module name cannot be empty");
}
Ok(name.trim().to_string())
}
fn scaffold_rust(path: &Path, name: &str) -> Result<()> {
run_cargo_init(path, name)?;
rewrite_cargo_toml(path, name)?;
write_lib_rs(path)?;
super::wasm_target::ensure_cargo_config(path)?;
Ok(())
}
fn run_cargo_init(path: &Path, name: &str) -> Result<()> {
let status = Command::new("cargo")
.args(["init", "--lib", "--name", name])
.current_dir(path)
.status()
.prefix_err(|| "Failed to execute `cargo init`")?;
if !status.success() {
anyhow::bail!("`cargo init` failed");
}
Ok(())
}
fn rewrite_cargo_toml(path: &Path, name: &str) -> Result<()> {
let cargo_toml = format!(
r#"[package]
name = "{name}"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
[dependencies]
anyhow = "1"
surrealism = "{surrealism}"
surrealdb-types = "{surrealdb}"
# surrealdb = {{ version = "{surrealdb}", default-features = false }} # optional; only if you need the full SDK crate
"#,
name = name,
surrealism = SURREALISM_VERSION,
surrealdb = SURREALDB_VERSION,
);
fs::write(path.join("Cargo.toml"), cargo_toml).prefix_err(|| "Failed to write Cargo.toml")?;
Ok(())
}
fn write_lib_rs(path: &Path) -> Result<()> {
let lib_rs = r#"use surrealism::surrealism;
#[surrealism(default)]
fn hello() -> String {
format!("Hello from Surrealism!")
}
#[surrealism]
fn greet(name: String) -> String {
format!("Hello, {name}!")
}
#[surrealism]
fn add(a: i64, b: i64) -> i64 {
a + b
}
"#;
fs::write(path.join("src/lib.rs"), lib_rs).prefix_err(|| "Failed to write src/lib.rs")?;
Ok(())
}
fn write_surrealism_toml(path: &Path, target: Target, org: &str, name: &str) -> Result<()> {
let config = SurrealismConfig {
target,
meta: SurrealismMeta {
organisation: org.to_string(),
name: name.to_string(),
version: Version::new(1, 0, 0),
},
capabilities: Default::default(),
abi: AbiVersion::CURRENT,
attach: Default::default(),
};
let toml = config.to_toml().prefix_err(|| "Failed to serialize surrealism.toml")?;
fs::write(path.join("surrealism.toml"), toml)
.prefix_err(|| "Failed to write surrealism.toml")?;
Ok(())
}