1use anyhow::Result;
2use clap::Parser;
3use std::path::{Path, PathBuf};
4use std::process::Command;
5use tracing::{debug, info, warn};
6
7use crate::{error::CliError, output::formatters::ProjectCreated, templates, CommandContext};
8
9#[derive(Debug, Parser)]
10pub struct CreateArgs {
11 #[arg(required = true)]
13 pub name: String,
14
15 #[arg(short, long, default_value = "basic")]
17 pub template: String,
18
19 #[arg(short, long)]
21 pub output_dir: Option<PathBuf>,
22
23 #[arg(long)]
25 pub skip_deps: bool,
26
27 #[arg(long)]
29 pub skip_build_check: bool,
30
31 #[arg(long)]
33 pub git: bool,
34
35 #[arg(long, conflicts_with = "git")]
37 pub no_git: bool,
38}
39
40pub async fn execute_async(args: &CreateArgs, ctx: &CommandContext) -> Result<(), CliError> {
42 debug!("Creating new actor project: {}", args.name);
43 debug!("Using template: {}", args.template);
44
45 if !is_valid_project_name(&args.name) {
47 return Err(CliError::invalid_input(
48 "project_name",
49 &args.name,
50 "Project names must only contain alphanumeric characters, hyphens, and underscores",
51 ));
52 }
53
54 let output_dir = match &args.output_dir {
56 Some(dir) => dir.clone(),
57 None => std::env::current_dir()
58 .map_err(|e| CliError::file_operation_failed("get current directory", ".", e))?,
59 };
60
61 debug!("Output directory: {}", output_dir.display());
62
63 let templates_list = templates::available_templates()
65 .map_err(|e| CliError::file_operation_failed("load templates", "templates directory", e))?;
66
67 if !templates_list.contains_key(&args.template) {
69 let available_templates: Vec<String> = templates_list.keys().cloned().collect();
70 return Err(CliError::template_not_found(
71 &args.template,
72 available_templates,
73 ));
74 }
75
76 let project_path = output_dir.join(&args.name);
78
79 if project_path.exists() {
81 return Err(CliError::file_operation_failed(
82 "create project",
83 project_path.display().to_string(),
84 std::io::Error::new(
85 std::io::ErrorKind::AlreadyExists,
86 "Directory already exists",
87 ),
88 ));
89 }
90
91 println!("Creating project structure...");
93 templates::create_project(&args.template, &args.name, &project_path).map_err(|e| {
94 CliError::file_operation_failed("create project", project_path.display().to_string(), e)
95 })?;
96 println!("✅ Project created from '{}' template", args.template);
97
98 if !args.skip_deps {
100 println!("\nFetching WIT dependencies...");
101 fetch_wit_dependencies(&project_path)?;
102 }
103
104 if !args.skip_deps && !args.skip_build_check {
106 println!("\nBuilding project...");
107 match build_project(&project_path) {
108 Ok(_) => println!("✅ Build successful"),
109 Err(e) => {
110 warn!("⚠️ Project created but initial build failed: {}", e);
111 warn!("You may need to run 'cargo build --target wasm32-unknown-unknown --release' manually");
112 }
113 }
114 }
115
116 if args.git || (!args.no_git && should_init_git()) {
118 println!("\nInitializing git repository...");
119 match init_git_repo(&project_path, &args.name) {
120 Ok(_) => println!("✅ Git repository initialized with initial commit"),
121 Err(e) => {
122 warn!("⚠️ Failed to initialize git repository: {}", e);
123 warn!("You can run 'git init' manually if needed");
124 }
125 }
126 }
127
128 println!("\nProject '{}' created successfully!", args.name);
130
131 let mut build_instructions = vec![format!("cd {}", args.name)];
133
134 if args.skip_deps {
135 build_instructions.push("wkg wit fetch".to_string());
136 }
137
138 build_instructions.extend(vec![
139 "cargo build --target wasm32-unknown-unknown --release".to_string(),
140 "theater spawn manifest.toml".to_string(),
141 ]);
142
143 if !args.git && (args.no_git || !should_init_git()) {
145 build_instructions.insert(1, "git init".to_string());
146 build_instructions.insert(2, "git add .".to_string());
147 build_instructions.insert(3, "git commit -m 'Initial commit'".to_string());
148 }
149
150 let result = ProjectCreated {
151 name: args.name.clone(),
152 template: args.template.clone(),
153 path: project_path,
154 build_instructions,
155 };
156
157 ctx.output.output(&result, None)?;
158 Ok(())
159}
160
161fn fetch_wit_dependencies(project_path: &PathBuf) -> Result<(), CliError> {
163 let child = Command::new("wkg")
165 .args(["wit", "fetch"])
166 .current_dir(project_path)
167 .spawn();
168
169 match child {
170 Ok(mut child) => {
171 let status = child.wait().map_err(|e| CliError::BuildFailed {
172 output: format!("Failed to wait for wkg wit fetch: {}", e),
173 })?;
174
175 if status.success() {
176 println!("✅ Dependencies fetched");
177 Ok(())
178 } else {
179 warn!("wkg wit fetch failed, trying alternative methods...");
180 try_wasm_tools_fetch(project_path)
181 }
182 }
183 Err(_) => {
184 warn!("wkg not found, trying alternative methods...");
185 try_wasm_tools_fetch(project_path)
186 }
187 }
188}
189
190fn try_wasm_tools_fetch(_project_path: &Path) -> Result<(), CliError> {
192 warn!("⚠️ Could not automatically fetch WIT dependencies");
194 warn!("Please run one of the following manually:");
195 warn!(" - wkg wit fetch (if you have wkg installed)");
196 warn!(" - Or manually download theater:simple WIT files to wit/deps/theater-simple/");
197
198 Ok(())
200}
201
202fn build_project(project_path: &PathBuf) -> Result<(), CliError> {
204 debug!("Building project at {}", project_path.display());
205
206 let mut child = Command::new("cargo")
207 .args(["build", "--target", "wasm32-unknown-unknown", "--release"])
208 .current_dir(project_path)
209 .spawn()
210 .map_err(|e| CliError::BuildFailed {
211 output: format!("Failed to execute cargo build: {}", e),
212 })?;
213
214 let status = child.wait().map_err(|e| CliError::BuildFailed {
215 output: format!("Failed to wait for cargo build: {}", e),
216 })?;
217
218 if !status.success() {
219 return Err(CliError::BuildFailed {
220 output: "Build failed - see output above for details".to_string(),
221 });
222 }
223
224 Ok(())
225}
226
227fn is_valid_project_name(name: &str) -> bool {
228 !name.is_empty()
230 && name
231 .chars()
232 .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
233}
234
235fn should_init_git() -> bool {
237 Command::new("git")
239 .args(["--version"])
240 .output()
241 .map(|output| output.status.success())
242 .unwrap_or(false)
243}
244
245fn init_git_repo(project_path: &PathBuf, project_name: &str) -> Result<(), CliError> {
247 debug!("Initializing git repository at {}", project_path.display());
248
249 let init_output = Command::new("git")
251 .args(["init"])
252 .current_dir(project_path)
253 .output()
254 .map_err(|_e| CliError::MissingTool {
255 tool: "git".to_string(),
256 install_command: "Install git from https://git-scm.com/".to_string(),
257 })?;
258
259 if !init_output.status.success() {
260 return Err(CliError::BuildFailed {
261 output: format!(
262 "Failed to initialize git repository: {}",
263 String::from_utf8_lossy(&init_output.stderr)
264 ),
265 });
266 }
267
268 let add_output = Command::new("git")
270 .args(["add", "."])
271 .current_dir(project_path)
272 .output()
273 .map_err(|e| CliError::BuildFailed {
274 output: format!("Failed to add files to git: {}", e),
275 })?;
276
277 if !add_output.status.success() {
278 return Err(CliError::BuildFailed {
279 output: format!(
280 "Failed to add files to git: {}",
281 String::from_utf8_lossy(&add_output.stderr)
282 ),
283 });
284 }
285
286 let commit_message = format!("Initial commit: Theater actor project '{}'", project_name);
288 let commit_output = Command::new("git")
289 .args(["commit", "-m", &commit_message])
290 .current_dir(project_path)
291 .output()
292 .map_err(|e| CliError::BuildFailed {
293 output: format!("Failed to make initial commit: {}", e),
294 })?;
295
296 if !commit_output.status.success() {
297 let stderr = String::from_utf8_lossy(&commit_output.stderr);
299 if stderr.contains("user.email") || stderr.contains("user.name") {
300 return Err(CliError::BuildFailed {
301 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(),
302 });
303 } else {
304 return Err(CliError::BuildFailed {
305 output: format!("Failed to make initial commit: {}", stderr),
306 });
307 }
308 }
309
310 info!("Git repository initialized with initial commit");
311 Ok(())
312}