1use 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
45 let signaling_url = match self.language {
47 SupportedLanguage::Kotlin | SupportedLanguage::Swift | SupportedLanguage::Python => {
48 self.signaling
49 .clone()
50 .unwrap_or_else(|| "wss://actrix1.develenv.com/signaling/ws".to_string())
51 }
52 SupportedLanguage::Rust => {
53 self.prompt_if_missing("signaling server URL", self.signaling.as_ref())?
54 }
55 };
56
57 let (project_dir, project_name) = self.resolve_project_info(&name)?;
58
59 info!("🚀 Initializing Actor-RTC project: {}", project_name);
60
61 if project_dir.exists() && project_dir != Path::new(".") {
63 return Err(ActrCliError::InvalidProject(format!(
64 "Directory '{}' already exists. Use a different name or remove the existing directory.",
65 project_dir.display()
66 )));
67 }
68
69 if project_dir == Path::new(".") && Path::new("Actr.toml").exists() {
71 return Err(ActrCliError::InvalidProject(
72 "Current directory already contains an Actor-RTC project (Actr.toml exists)"
73 .to_string(),
74 ));
75 }
76
77 if project_dir != Path::new(".") {
79 std::fs::create_dir_all(&project_dir)?;
80 }
81
82 if self.language != SupportedLanguage::Rust {
83 let context = InitContext {
84 project_dir: project_dir.clone(),
85 project_name: project_name.clone(),
86 signaling_url: signaling_url.clone(),
87 template: self.template,
88 is_current_dir: project_dir == Path::new("."),
89 };
90 initialize::execute_initialize(self.language, &context).await?;
91 info!(
92 "✅ Successfully created Actor-RTC project '{}'",
93 project_name
94 );
95 return Ok(());
96 }
97
98 self.generate_project_structure(
100 &project_dir,
101 &project_name,
102 &signaling_url,
103 self.template,
104 )?;
105
106 info!(
107 "✅ Successfully created Actor-RTC project '{}'",
108 project_name
109 );
110 if project_dir != Path::new(".") {
111 info!("📁 Project created in: {}", project_dir.display());
112 info!("");
113 info!("Next steps:");
114 info!(" cd {}/client", project_dir.display());
115 info!(" actr install # Install remote protobuf dependencies from Actr.toml");
116 info!(" actr gen # Generate Actor code");
117 info!(" cargo run # Start your work");
118 } else {
119 info!("📁 Project initialized in current directory");
120 info!("");
121 info!("Next steps:");
122 info!(" actr install # Install remote protobuf dependencies from Actr.toml");
123 info!(" actr gen # Generate Actor code");
124 info!(" cargo run # Start your work");
125 }
126
127 Ok(())
128 }
129}
130
131impl InitCommand {
132 fn resolve_project_info(&self, name: &str) -> Result<(PathBuf, String)> {
133 if name == "." {
134 let project_name = if let Some(name) = &self.project_name {
136 name.clone()
137 } else {
138 let current_dir = std::env::current_dir().map_err(|e| {
139 ActrCliError::InvalidProject(format!(
140 "Failed to resolve current directory: {e}"
141 ))
142 })?;
143 current_dir
144 .file_name()
145 .and_then(|s| s.to_str())
146 .map(|s| s.to_string())
147 .ok_or_else(|| {
148 ActrCliError::InvalidProject(
149 "Failed to infer project name from current directory".to_string(),
150 )
151 })?
152 };
153 Ok((PathBuf::from("."), project_name))
154 } else {
155 let path = PathBuf::from(name);
157 let project_name = path
158 .file_name()
159 .and_then(|s| s.to_str())
160 .unwrap_or(name)
161 .to_string();
162 Ok((path, project_name))
163 }
164 }
165
166 fn generate_project_structure(
167 &self,
168 project_dir: &Path,
169 project_name: &str,
170 signaling_url: &str,
171 template: ProjectTemplateName,
172 ) -> Result<()> {
173 if project_dir == Path::new(".") {
175 self.init_with_cargo(project_dir, None, signaling_url, template)?;
177 } else {
178 std::fs::create_dir_all(project_dir)?;
180 self.init_with_cargo(project_dir, Some(project_name), signaling_url, template)?;
181 }
182
183 Ok(())
184 }
185
186 fn create_actr_config(
187 &self,
188 project_dir: &Path,
189 project_name: &str,
190 signaling_url: &str,
191 ) -> Result<()> {
192 let service_type = format!("{project_name}-service");
193
194 let actr_toml_content = format!(
196 r#"edition = 1
197exports = []
198
199[package]
200name = "{project_name}"
201manufacturer = "my-company"
202type = "{service_type}"
203description = "An Actor-RTC service"
204authors = []
205
206[dependencies]
207
208[system.signaling]
209url = "{signaling_url}"
210
211[system.deployment]
212realm_id = 1001
213
214[system.discovery]
215visible = true
216
217[scripts]
218dev = "cargo run"
219test = "cargo test"
220"#
221 );
222
223 std::fs::write(project_dir.join("Actr.toml"), actr_toml_content)?;
224
225 info!("📄 Created Actr.toml configuration");
226 Ok(())
227 }
228
229 fn create_gitignore(&self, project_dir: &Path) -> Result<()> {
230 let gitignore_content = r#"/target
231/Cargo.lock
232.env
233.env.local
234*.log
235.DS_Store
236/src/generated/
237"#;
238
239 std::fs::write(project_dir.join(".gitignore"), gitignore_content)?;
240
241 info!("📄 Created .gitignore");
242 Ok(())
243 }
244
245 fn prompt_if_missing(
247 &self,
248 field_name: &str,
249 current_value: Option<&String>,
250 ) -> Result<String> {
251 if let Some(value) = current_value {
252 return Ok(value.clone());
253 }
254
255 match field_name {
256 "project name" => {
257 println!("┌──────────────────────────────────────────────────────────┐");
258 println!("│ 📋 Project Name Configuration │");
259 println!("├──────────────────────────────────────────────────────────┤");
260 println!("│ │");
261 println!("│ 📝 Requirements: │");
262 println!("│ • Only alphanumeric characters, hyphens and _ │");
263 println!("│ • Cannot start or end with - or _ │");
264 println!("│ │");
265 println!("│ 💡 Examples: │");
266 println!("│ my-chat-service, user-manager, media_streamer │");
267 println!("│ │");
268 println!("└──────────────────────────────────────────────────────────┘");
269 print!("🎯 Enter project name [my-actor-project]: ");
270 }
271 "signaling server URL" => {
272 println!("┌──────────────────────────────────────────────────────────┐");
273 println!("│ 🌐 Signaling Server Configuration │");
274 println!("├──────────────────────────────────────────────────────────┤");
275 println!("│ │");
276 println!("│ 📡 WebSocket URL for Actor-RTC signaling coordination │");
277 println!("│ │");
278 println!("│ 💡 Examples: │");
279 println!("│ ws://localhost:8080/ (development) │");
280 println!("│ wss://example.com (production │");
281 println!("│ wss://example.com/?token=${{TOKEN}} (with auth) │");
282 println!("│ │");
283 println!("└──────────────────────────────────────────────────────────┘");
284 print!("🎯 Enter signaling server URL [wss://actrix1.develenv.com]: ");
285 }
286 _ => {
287 print!("🎯 Enter {field_name}: ");
288 }
289 }
290
291 io::stdout().flush().map_err(ActrCliError::Io)?;
292
293 let mut input = String::new();
294 io::stdin()
295 .read_line(&mut input)
296 .map_err(ActrCliError::Io)?;
297
298 println!();
299
300 let trimmed = input.trim();
301 if trimmed.is_empty() {
302 let default = match field_name {
304 "project name" => "my-actor-project",
305 "signaling server URL" => "wss://actrix1.develenv.com",
306 _ => {
307 return Err(ActrCliError::InvalidProject(format!(
308 "{field_name} cannot be empty"
309 )));
310 }
311 };
312 Ok(default.to_string())
313 } else {
314 if field_name == "project name" {
316 self.validate_project_name(trimmed)?;
317 }
318 Ok(trimmed.to_string())
319 }
320 }
321
322 fn validate_project_name(&self, name: &str) -> Result<()> {
324 let is_valid = name
326 .chars()
327 .all(|c| c.is_alphanumeric() || c == '-' || c == '_');
328
329 if !is_valid {
330 return Err(ActrCliError::InvalidProject(format!(
331 "Invalid project name '{name}'. Only alphanumeric characters, hyphens, and underscores are allowed."
332 )));
333 }
334
335 if name.is_empty() {
337 return Err(ActrCliError::InvalidProject(
338 "Project name cannot be empty".to_string(),
339 ));
340 }
341
342 if name.starts_with('-') || name.ends_with('-') {
343 return Err(ActrCliError::InvalidProject(
344 "Project name cannot start or end with a hyphen".to_string(),
345 ));
346 }
347
348 if name.starts_with('_') || name.ends_with('_') {
349 return Err(ActrCliError::InvalidProject(
350 "Project name cannot start or end with an underscore".to_string(),
351 ));
352 }
353
354 Ok(())
355 }
356
357 fn init_with_cargo(
359 &self,
360 project_dir: &Path,
361 explicit_name: Option<&str>,
362 signaling_url: &str,
363 template: ProjectTemplateName,
364 ) -> Result<()> {
365 info!("🚀 Initializing Rust project with cargo...");
366
367 let mut cmd = std::process::Command::new("cargo");
369 cmd.arg("init").arg("--quiet").current_dir(project_dir);
370
371 if let Some(name) = explicit_name {
373 cmd.arg("--name").arg(name);
374 }
375
376 let cargo_result = cmd
377 .output()
378 .map_err(|e| ActrCliError::Command(format!("Failed to run cargo init: {e}")))?;
379
380 if !cargo_result.status.success() {
381 let error_msg = String::from_utf8_lossy(&cargo_result.stderr);
382 return Err(ActrCliError::Command(format!(
383 "cargo init failed: {error_msg}"
384 )));
385 }
386
387 let project_name = self.extract_project_name_from_cargo_toml(project_dir)?;
389 info!("📦 Rust project initialized: '{}'", project_name);
390
391 self.enhance_cargo_project_for_actr(project_dir, &project_name, signaling_url, template)?;
393
394 Ok(())
395 }
396
397 fn extract_project_name_from_cargo_toml(&self, project_dir: &Path) -> Result<String> {
399 let cargo_toml_path = project_dir.join("Cargo.toml");
400 let cargo_content = std::fs::read_to_string(&cargo_toml_path).map_err(ActrCliError::Io)?;
401
402 for line in cargo_content.lines() {
404 if line.trim().starts_with("name = ")
405 && let Some(name_part) = line.split('=').nth(1)
406 {
407 let name = name_part.trim().trim_matches('"').trim_matches('\'');
408 return Ok(name.to_string());
409 }
410 }
411
412 Ok("actor-service".to_string())
414 }
415
416 fn enhance_cargo_project_for_actr(
418 &self,
419 project_dir: &Path,
420 project_name: &str,
421 signaling_url: &str,
422 template: ProjectTemplateName,
423 ) -> Result<()> {
424 info!("⚡ Enhancing with Actor-RTC features...");
425
426 let proto_dir = project_dir.join("protos");
428 std::fs::create_dir_all(&proto_dir)?;
429 info!("📁 Created protos/ directory");
430
431 crate::commands::initialize::create_local_proto(
433 project_dir,
434 project_name,
435 "protos/local",
436 template,
437 )?;
438
439 self.create_actr_config(project_dir, project_name, signaling_url)?;
441 info!("📄 Created Actr.toml configuration");
442
443 self.enhance_cargo_toml_with_actr_deps(project_dir)?;
445 info!("📦 Enhanced Cargo.toml with Actor-RTC dependencies");
446
447 let gitignore_path = project_dir.join(".gitignore");
449 if !gitignore_path.exists() {
450 self.create_gitignore(project_dir)?;
451 info!("📄 Created .gitignore");
452 }
453
454 Ok(())
455 }
456
457 fn enhance_cargo_toml_with_actr_deps(&self, project_dir: &Path) -> Result<()> {
459 let cargo_toml_path = project_dir.join("Cargo.toml");
460 let mut cargo_content =
461 std::fs::read_to_string(&cargo_toml_path).map_err(ActrCliError::Io)?;
462
463 if !cargo_content.contains("actr-core") {
465 cargo_content.push_str("\n# Actor-RTC Framework Dependencies\n");
466 cargo_content.push_str("actr-core = { path = \"../actr-core\" }\n");
467 cargo_content.push_str("tokio = { version = \"1.0\", features = [\"full\"] }\n");
468
469 std::fs::write(&cargo_toml_path, cargo_content).map_err(ActrCliError::Io)?;
470 }
471
472 Ok(())
473 }
474}