use anyhow::Result;
use clap::Parser;
use std::path::{Path, PathBuf};
use std::process::Command;
use tracing::{debug, info, warn};
use crate::{error::CliError, output::formatters::ProjectCreated, templates, CommandContext};
#[derive(Debug, Parser)]
pub struct CreateArgs {
#[arg(required = true)]
pub name: String,
#[arg(short, long, default_value = "basic")]
pub template: String,
#[arg(short, long)]
pub output_dir: Option<PathBuf>,
#[arg(long)]
pub skip_deps: bool,
#[arg(long)]
pub skip_build_check: bool,
#[arg(long)]
pub git: bool,
#[arg(long, conflicts_with = "git")]
pub no_git: bool,
}
pub async fn execute_async(args: &CreateArgs, ctx: &CommandContext) -> Result<(), CliError> {
debug!("Creating new actor project: {}", args.name);
debug!("Using template: {}", args.template);
if !is_valid_project_name(&args.name) {
return Err(CliError::invalid_input(
"project_name",
&args.name,
"Project names must only contain alphanumeric characters, hyphens, and underscores",
));
}
let output_dir = match &args.output_dir {
Some(dir) => dir.clone(),
None => std::env::current_dir()
.map_err(|e| CliError::file_operation_failed("get current directory", ".", e))?,
};
debug!("Output directory: {}", output_dir.display());
let templates_list = templates::available_templates()
.map_err(|e| CliError::file_operation_failed("load templates", "templates directory", e))?;
if !templates_list.contains_key(&args.template) {
let available_templates: Vec<String> = templates_list.keys().cloned().collect();
return Err(CliError::template_not_found(
&args.template,
available_templates,
));
}
let project_path = output_dir.join(&args.name);
if project_path.exists() {
return Err(CliError::file_operation_failed(
"create project",
project_path.display().to_string(),
std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
"Directory already exists",
),
));
}
println!("Creating project structure...");
templates::create_project(&args.template, &args.name, &project_path).map_err(|e| {
CliError::file_operation_failed("create project", project_path.display().to_string(), e)
})?;
println!("✅ Project created from '{}' template", args.template);
if !args.skip_deps {
println!("\nFetching WIT dependencies...");
fetch_wit_dependencies(&project_path)?;
}
if !args.skip_deps && !args.skip_build_check {
println!("\nBuilding project...");
match build_project(&project_path) {
Ok(_) => println!("✅ Build successful"),
Err(e) => {
warn!("⚠️ Project created but initial build failed: {}", e);
warn!("You may need to run 'cargo build --target wasm32-unknown-unknown --release' manually");
}
}
}
if args.git || (!args.no_git && should_init_git()) {
println!("\nInitializing git repository...");
match init_git_repo(&project_path, &args.name) {
Ok(_) => println!("✅ Git repository initialized with initial commit"),
Err(e) => {
warn!("⚠️ Failed to initialize git repository: {}", e);
warn!("You can run 'git init' manually if needed");
}
}
}
println!("\nProject '{}' created successfully!", args.name);
let mut build_instructions = vec![format!("cd {}", args.name)];
if args.skip_deps {
build_instructions.push("wkg wit fetch".to_string());
}
build_instructions.extend(vec![
"cargo build --target wasm32-unknown-unknown --release".to_string(),
"theater spawn manifest.toml".to_string(),
]);
if !args.git && (args.no_git || !should_init_git()) {
build_instructions.insert(1, "git init".to_string());
build_instructions.insert(2, "git add .".to_string());
build_instructions.insert(3, "git commit -m 'Initial commit'".to_string());
}
let result = ProjectCreated {
name: args.name.clone(),
template: args.template.clone(),
path: project_path,
build_instructions,
};
ctx.output.output(&result, None)?;
Ok(())
}
fn fetch_wit_dependencies(project_path: &PathBuf) -> Result<(), CliError> {
let child = Command::new("wkg")
.args(["wit", "fetch"])
.current_dir(project_path)
.spawn();
match child {
Ok(mut child) => {
let status = child.wait().map_err(|e| CliError::BuildFailed {
output: format!("Failed to wait for wkg wit fetch: {}", e),
})?;
if status.success() {
println!("✅ Dependencies fetched");
Ok(())
} else {
warn!("wkg wit fetch failed, trying alternative methods...");
try_wasm_tools_fetch(project_path)
}
}
Err(_) => {
warn!("wkg not found, trying alternative methods...");
try_wasm_tools_fetch(project_path)
}
}
}
fn try_wasm_tools_fetch(_project_path: &Path) -> Result<(), CliError> {
warn!("⚠️ Could not automatically fetch WIT dependencies");
warn!("Please run one of the following manually:");
warn!(" - wkg wit fetch (if you have wkg installed)");
warn!(" - Or manually download theater:simple WIT files to wit/deps/theater-simple/");
Ok(())
}
fn build_project(project_path: &PathBuf) -> Result<(), CliError> {
debug!("Building project at {}", project_path.display());
let mut child = Command::new("cargo")
.args(["build", "--target", "wasm32-unknown-unknown", "--release"])
.current_dir(project_path)
.spawn()
.map_err(|e| CliError::BuildFailed {
output: format!("Failed to execute cargo build: {}", e),
})?;
let status = child.wait().map_err(|e| CliError::BuildFailed {
output: format!("Failed to wait for cargo build: {}", e),
})?;
if !status.success() {
return Err(CliError::BuildFailed {
output: "Build failed - see output above for details".to_string(),
});
}
Ok(())
}
fn is_valid_project_name(name: &str) -> bool {
!name.is_empty()
&& name
.chars()
.all(|c| c.is_alphanumeric() || c == '-' || c == '_')
}
fn should_init_git() -> bool {
Command::new("git")
.args(["--version"])
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
fn init_git_repo(project_path: &PathBuf, project_name: &str) -> Result<(), CliError> {
debug!("Initializing git repository at {}", project_path.display());
let init_output = Command::new("git")
.args(["init"])
.current_dir(project_path)
.output()
.map_err(|_e| CliError::MissingTool {
tool: "git".to_string(),
install_command: "Install git from https://git-scm.com/".to_string(),
})?;
if !init_output.status.success() {
return Err(CliError::BuildFailed {
output: format!(
"Failed to initialize git repository: {}",
String::from_utf8_lossy(&init_output.stderr)
),
});
}
let add_output = Command::new("git")
.args(["add", "."])
.current_dir(project_path)
.output()
.map_err(|e| CliError::BuildFailed {
output: format!("Failed to add files to git: {}", e),
})?;
if !add_output.status.success() {
return Err(CliError::BuildFailed {
output: format!(
"Failed to add files to git: {}",
String::from_utf8_lossy(&add_output.stderr)
),
});
}
let commit_message = format!("Initial commit: Theater actor project '{}'", project_name);
let commit_output = Command::new("git")
.args(["commit", "-m", &commit_message])
.current_dir(project_path)
.output()
.map_err(|e| CliError::BuildFailed {
output: format!("Failed to make initial commit: {}", e),
})?;
if !commit_output.status.success() {
let stderr = String::from_utf8_lossy(&commit_output.stderr);
if stderr.contains("user.email") || stderr.contains("user.name") {
return Err(CliError::BuildFailed {
output: "Git commit failed: Please configure git with 'git config --global user.name \"Your Name\"' and 'git config --global user.email \"your.email@example.com\"'".to_string(),
});
} else {
return Err(CliError::BuildFailed {
output: format!("Failed to make initial commit: {}", stderr),
});
}
}
info!("Git repository initialized with initial commit");
Ok(())
}