use anyhow::{Context, Result};
use include_dir::{include_dir, Dir};
use std::fs;
use std::path::{Path, PathBuf};
pub(crate) static TEMPLATE_DIR: Dir<'static> =
include_dir!("$OUT_DIR/templates/default");
const _TEMPLATE_STAMP: &str = include_str!(concat!(env!("OUT_DIR"), "/template_stamp.txt"));
pub fn run(name: &str) -> Result<()> {
let project_dir = PathBuf::from(name);
if project_dir.exists() {
anyhow::bail!("Directory '{}' already exists", name);
}
println!("Creating new Arcane project: {}", name);
match find_template_dir() {
Some(template_dir) => copy_template_fs(&template_dir, &project_dir, name)?,
None => copy_template_embedded(&TEMPLATE_DIR, &project_dir, name)?,
}
println!("✓ Created {}/", name);
println!();
println!("Next steps:");
println!(" cd {}", name);
println!(" npm install");
println!(" arcane dev");
println!();
println!("MCP auto-configured for Claude Code, Cursor, and VS Code.");
println!("AI tools will discover your game engine automatically."); println!();
println!("Read AGENTS.md for LLM development guide.");
println!("API reference in types/*.d.ts (one file per module).");
println!();
println!("Happy game building! 🎮");
Ok(())
}
pub(crate) fn find_template_dir() -> Option<PathBuf> {
if let Ok(exe) = std::env::current_exe() {
let mut dir = exe.parent().map(|p| p.to_path_buf());
while let Some(d) = dir {
let candidate = d.join("templates").join("default");
if candidate.exists() && candidate.join("package.json").exists() {
return Some(candidate);
}
dir = d.parent().map(|p| p.to_path_buf());
}
}
let candidate = PathBuf::from("templates").join("default");
if candidate.exists() && candidate.join("package.json").exists() {
return Some(candidate);
}
None
}
fn copy_template_fs(src: &Path, dst: &Path, project_name: &str) -> Result<()> {
fs::create_dir_all(dst)?;
for entry in fs::read_dir(src)? {
let entry = entry?;
let src_path = entry.path();
let file_name = entry.file_name();
let dst_path = dst.join(&file_name);
if src_path.is_dir() {
copy_template_fs(&src_path, &dst_path, project_name)?;
} else {
let content = fs::read_to_string(&src_path).with_context(|| {
format!("Failed to read template file: {}", src_path.display())
})?;
let processed = content.replace("{{PROJECT_NAME}}", project_name);
fs::write(&dst_path, processed)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let src_metadata = fs::metadata(&src_path)?;
let mut permissions = fs::metadata(&dst_path)?.permissions();
permissions.set_mode(src_metadata.permissions().mode());
fs::set_permissions(&dst_path, permissions)?;
}
}
}
Ok(())
}
pub(crate) fn copy_template_embedded(
dir: &Dir<'_>,
dst: &Path,
project_name: &str,
) -> Result<()> {
fs::create_dir_all(dst)?;
for file in dir.files() {
let file_path = file.path();
if file_path.starts_with("default") {
continue;
}
let dst_path = dst.join(file_path);
if let Some(parent) = dst_path.parent() {
fs::create_dir_all(parent)?;
}
let content = file
.contents_utf8()
.with_context(|| format!("Template file not valid UTF-8: {:?}", file_path))?;
let processed = content.replace("{{PROJECT_NAME}}", project_name);
fs::write(&dst_path, processed)?;
}
for subdir in dir.dirs() {
if subdir.path() == Path::new("default") {
continue;
}
copy_template_embedded(subdir, dst, project_name)?;
}
Ok(())
}