actr_cli/commands/
init.rs1use crate::commands::initialize::{self, InitContext};
4use crate::commands::{Command, SupportedLanguage};
5use crate::error::{ActrCliError, Result};
6use crate::template::ProjectTemplateName;
7use async_trait::async_trait;
8use clap::Args;
9use std::io::{self, Write};
10use std::path::{Path, PathBuf};
11use tracing::info;
12
13#[derive(Args)]
14pub struct InitCommand {
15 pub name: Option<String>,
17
18 #[arg(long, default_value_t = ProjectTemplateName::Echo)]
20 pub template: ProjectTemplateName,
21
22 #[arg(long)]
24 pub project_name: Option<String>,
25
26 #[arg(long)]
28 pub signaling: Option<String>,
29
30 #[arg(short, long, default_value = "rust")]
32 pub language: SupportedLanguage,
33}
34
35#[async_trait]
36impl Command for InitCommand {
37 async fn execute(&self) -> Result<()> {
38 println!("🎯 Actor-RTC Project Initialization");
40 println!("----------------------------------------");
41
42 let name = self.prompt_if_missing("project name", self.name.as_ref())?;
44 let signaling_url =
45 self.prompt_if_missing("signaling server URL", self.signaling.as_ref())?;
46
47 let (project_dir, project_name) = self.resolve_project_info(&name)?;
48
49 info!("🚀 Initializing Actor-RTC project: {}", project_name);
50
51 if project_dir.exists() && project_dir != Path::new(".") {
53 return Err(ActrCliError::InvalidProject(format!(
54 "Directory '{}' already exists. Use a different name or remove the existing directory.",
55 project_dir.display()
56 )));
57 }
58
59 if project_dir == Path::new(".") && Path::new("Actr.toml").exists() {
61 return Err(ActrCliError::InvalidProject(
62 "Current directory already contains an Actor-RTC project (Actr.toml exists)"
63 .to_string(),
64 ));
65 }
66
67 if project_dir != Path::new(".") {
69 std::fs::create_dir_all(&project_dir)?;
70 }
71
72 if self.language != SupportedLanguage::Rust {
73 let context = InitContext {
74 project_dir: project_dir.clone(),
75 project_name: project_name.clone(),
76 signaling_url: signaling_url.clone(),
77 template: self.template,
78 is_current_dir: project_dir == Path::new("."),
79 };
80 initialize::execute_initialize(self.language, &context)?;
81 info!(
82 "✅ Successfully created Actor-RTC project '{}'",
83 project_name
84 );
85 return Ok(());
86 }
87
88 self.generate_project_structure(&project_dir, &project_name, &signaling_url)?;
90
91 info!(
92 "✅ Successfully created Actor-RTC project '{}'",
93 project_name
94 );
95 if project_dir != Path::new(".") {
96 info!("📁 Project created in: {}", project_dir.display());
97 info!("");
98 info!("Next steps:");
99 info!(" cd {}", project_dir.display());
100 info!(" actr install actr://{{some-service}}/ # Add service dependencies");
101 info!(" actr gen # Generate Actor code");
102 info!(" cargo run # Start your work");
103 } else {
104 info!("📁 Project initialized in current directory");
105 info!("");
106 info!("Next steps:");
107 info!(" actr install actr://{{some-service}}/ # Add service dependencies");
108 info!(" actr gen # Generate Actor code");
109 info!(" cargo run # Start your work");
110 }
111
112 Ok(())
113 }
114}
115
116impl InitCommand {
117 fn resolve_project_info(&self, name: &str) -> Result<(PathBuf, String)> {
118 if name == "." {
119 let project_name = if let Some(name) = &self.project_name {
121 name.clone()
122 } else {
123 "current-dir".to_string() };
126 Ok((PathBuf::from("."), project_name))
127 } else {
128 let path = PathBuf::from(name);
130 let project_name = path
131 .file_name()
132 .and_then(|s| s.to_str())
133 .unwrap_or(name)
134 .to_string();
135 Ok((path, project_name))
136 }
137 }
138
139 fn generate_project_structure(
140 &self,
141 project_dir: &Path,
142 project_name: &str,
143 signaling_url: &str,
144 ) -> Result<()> {
145 if project_dir == Path::new(".") {
147 self.init_with_cargo(project_dir, None, signaling_url)?;
149 } else {
150 std::fs::create_dir_all(project_dir)?;
152 self.init_with_cargo(project_dir, Some(project_name), signaling_url)?;
153 }
154
155 Ok(())
156 }
157
158 fn create_actr_config(
159 &self,
160 project_dir: &Path,
161 project_name: &str,
162 signaling_url: &str,
163 ) -> Result<()> {
164 let service_type = format!("{project_name}-service");
165
166 let actr_toml_content = format!(
168 r#"edition = 1
169exports = []
170
171[package]
172name = "{project_name}"
173manufacturer = "my-company"
174type = "{service_type}"
175description = "An Actor-RTC service"
176authors = []
177
178[dependencies]
179
180[system.signaling]
181url = "{signaling_url}"
182
183[system.deployment]
184realm = 1001
185
186[system.discovery]
187visible = true
188
189[scripts]
190dev = "cargo run"
191test = "cargo test"
192"#
193 );
194
195 std::fs::write(project_dir.join("Actr.toml"), actr_toml_content)?;
196
197 info!("📄 Created Actr.toml configuration");
198 Ok(())
199 }
200
201 fn create_gitignore(&self, project_dir: &Path) -> Result<()> {
202 let gitignore_content = r#"/target
203/Cargo.lock
204.env
205.env.local
206*.log
207.DS_Store
208/src/generated/
209"#;
210
211 std::fs::write(project_dir.join(".gitignore"), gitignore_content)?;
212
213 info!("📄 Created .gitignore");
214 Ok(())
215 }
216
217 fn prompt_if_missing(
219 &self,
220 field_name: &str,
221 current_value: Option<&String>,
222 ) -> Result<String> {
223 if let Some(value) = current_value {
224 return Ok(value.clone());
225 }
226
227 match field_name {
228 "project name" => {
229 println!("┌──────────────────────────────────────────────────────────┐");
230 println!("│ 📋 Project Name Configuration │");
231 println!("├──────────────────────────────────────────────────────────┤");
232 println!("│ │");
233 println!("│ 📝 Requirements: │");
234 println!("│ • Only alphanumeric characters, hyphens and _ │");
235 println!("│ • Cannot start or end with - or _ │");
236 println!("│ │");
237 println!("│ 💡 Examples: │");
238 println!("│ my-chat-service, user-manager, media_streamer │");
239 println!("│ │");
240 println!("└──────────────────────────────────────────────────────────┘");
241 print!("🎯 Enter project name [my-actor-project]: ");
242 }
243 "signaling server URL" => {
244 println!("┌──────────────────────────────────────────────────────────┐");
245 println!("│ 🌐 Signaling Server Configuration │");
246 println!("├──────────────────────────────────────────────────────────┤");
247 println!("│ │");
248 println!("│ 📡 WebSocket URL for Actor-RTC signaling coordination │");
249 println!("│ │");
250 println!("│ 💡 Examples: │");
251 println!("│ ws://localhost:8080/ (development) │");
252 println!("│ wss://example.com (production │");
253 println!("│ wss://example.com/?token=${{TOKEN}} (with auth) │");
254 println!("│ │");
255 println!("└──────────────────────────────────────────────────────────┘");
256 print!("🎯 Enter signaling server URL [ws://localhost:8080]: ");
257 }
258 _ => {
259 print!("🎯 Enter {field_name}: ");
260 }
261 }
262
263 io::stdout().flush().map_err(ActrCliError::Io)?;
264
265 let mut input = String::new();
266 io::stdin()
267 .read_line(&mut input)
268 .map_err(ActrCliError::Io)?;
269
270 println!();
271
272 let trimmed = input.trim();
273 if trimmed.is_empty() {
274 let default = match field_name {
276 "project name" => "my-actor-project",
277 "signaling server URL" => "ws://localhost:8080",
278 _ => {
279 return Err(ActrCliError::InvalidProject(format!(
280 "{field_name} cannot be empty"
281 )));
282 }
283 };
284 Ok(default.to_string())
285 } else {
286 if field_name == "project name" {
288 self.validate_project_name(trimmed)?;
289 }
290 Ok(trimmed.to_string())
291 }
292 }
293
294 fn validate_project_name(&self, name: &str) -> Result<()> {
296 let is_valid = name
298 .chars()
299 .all(|c| c.is_alphanumeric() || c == '-' || c == '_');
300
301 if !is_valid {
302 return Err(ActrCliError::InvalidProject(format!(
303 "Invalid project name '{name}'. Only alphanumeric characters, hyphens, and underscores are allowed."
304 )));
305 }
306
307 if name.is_empty() {
309 return Err(ActrCliError::InvalidProject(
310 "Project name cannot be empty".to_string(),
311 ));
312 }
313
314 if name.starts_with('-') || name.ends_with('-') {
315 return Err(ActrCliError::InvalidProject(
316 "Project name cannot start or end with a hyphen".to_string(),
317 ));
318 }
319
320 if name.starts_with('_') || name.ends_with('_') {
321 return Err(ActrCliError::InvalidProject(
322 "Project name cannot start or end with an underscore".to_string(),
323 ));
324 }
325
326 Ok(())
327 }
328
329 fn init_with_cargo(
331 &self,
332 project_dir: &Path,
333 explicit_name: Option<&str>,
334 signaling_url: &str,
335 ) -> Result<()> {
336 info!("🚀 Initializing Rust project with cargo...");
337
338 let mut cmd = std::process::Command::new("cargo");
340 cmd.arg("init").arg("--quiet").current_dir(project_dir);
341
342 if let Some(name) = explicit_name {
344 cmd.arg("--name").arg(name);
345 }
346
347 let cargo_result = cmd
348 .output()
349 .map_err(|e| ActrCliError::Command(format!("Failed to run cargo init: {e}")))?;
350
351 if !cargo_result.status.success() {
352 let error_msg = String::from_utf8_lossy(&cargo_result.stderr);
353 return Err(ActrCliError::Command(format!(
354 "cargo init failed: {error_msg}"
355 )));
356 }
357
358 let project_name = self.extract_project_name_from_cargo_toml(project_dir)?;
360 info!("📦 Rust project initialized: '{}'", project_name);
361
362 self.enhance_cargo_project_for_actr(project_dir, &project_name, signaling_url)?;
364
365 Ok(())
366 }
367
368 fn extract_project_name_from_cargo_toml(&self, project_dir: &Path) -> Result<String> {
370 let cargo_toml_path = project_dir.join("Cargo.toml");
371 let cargo_content = std::fs::read_to_string(&cargo_toml_path).map_err(ActrCliError::Io)?;
372
373 for line in cargo_content.lines() {
375 if line.trim().starts_with("name = ")
376 && let Some(name_part) = line.split('=').nth(1)
377 {
378 let name = name_part.trim().trim_matches('"').trim_matches('\'');
379 return Ok(name.to_string());
380 }
381 }
382
383 Ok("actor-service".to_string())
385 }
386
387 fn enhance_cargo_project_for_actr(
389 &self,
390 project_dir: &Path,
391 project_name: &str,
392 signaling_url: &str,
393 ) -> Result<()> {
394 info!("⚡ Enhancing with Actor-RTC features...");
395
396 let proto_dir = project_dir.join("proto");
398 std::fs::create_dir_all(&proto_dir)?;
399 info!("📁 Created proto/ directory");
400
401 self.create_actr_config(project_dir, project_name, signaling_url)?;
403 info!("📄 Created Actr.toml configuration");
404
405 self.enhance_cargo_toml_with_actr_deps(project_dir)?;
407 info!("📦 Enhanced Cargo.toml with Actor-RTC dependencies");
408
409 let gitignore_path = project_dir.join(".gitignore");
411 if !gitignore_path.exists() {
412 self.create_gitignore(project_dir)?;
413 info!("📄 Created .gitignore");
414 }
415
416 Ok(())
417 }
418
419 fn enhance_cargo_toml_with_actr_deps(&self, project_dir: &Path) -> Result<()> {
421 let cargo_toml_path = project_dir.join("Cargo.toml");
422 let mut cargo_content =
423 std::fs::read_to_string(&cargo_toml_path).map_err(ActrCliError::Io)?;
424
425 if !cargo_content.contains("actr-core") {
427 cargo_content.push_str("\n# Actor-RTC Framework Dependencies\n");
428 cargo_content.push_str("actr-core = { path = \"../actr-core\" }\n");
429 cargo_content.push_str("tokio = { version = \"1.0\", features = [\"full\"] }\n");
430
431 std::fs::write(&cargo_toml_path, cargo_content).map_err(ActrCliError::Io)?;
432 }
433
434 Ok(())
435 }
436}