actr_cli/commands/
init.rs

1//! Project initialization command
2
3use 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    /// Name of the project to create (use '.' for current directory)
16    pub name: Option<String>,
17
18    /// Project template to use (echo)
19    #[arg(long, default_value_t = ProjectTemplateName::Echo)]
20    pub template: ProjectTemplateName,
21
22    /// Project name when initializing in current directory
23    #[arg(long)]
24    pub project_name: Option<String>,
25
26    /// Signaling server URL
27    #[arg(long)]
28    pub signaling: Option<String>,
29
30    /// Target language for project initialization
31    #[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        // Show welcome header
39        println!("🎯 Actor-RTC Project Initialization");
40        println!("----------------------------------------");
41
42        // Interactive prompt for missing required fields
43        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        // Check if target directory exists and is not empty
52        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        // Check if current directory already has Actr.toml
60        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        // Create project directory if needed
68        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        // Generate project structure
89        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            // Initialize in current directory - cargo will determine the name
120            let project_name = if let Some(name) = &self.project_name {
121                name.clone()
122            } else {
123                // Let cargo determine the project name from directory
124                "current-dir".to_string() // Placeholder - cargo will override
125            };
126            Ok((PathBuf::from("."), project_name))
127        } else {
128            // Create new directory - extract project name from path
129            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        // Always use cargo init for all scenarios
146        if project_dir == Path::new(".") {
147            // Current directory init - let cargo handle naming
148            self.init_with_cargo(project_dir, None, signaling_url)?;
149        } else {
150            // New directory - create it and use cargo init with explicit name
151            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        // Create Actr.toml directly as string (Config doesn't have default_template or save_to_file)
167        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    /// Interactive prompt for missing fields with detailed guidance
218    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            // Provide sensible defaults
275            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            // Validate project name if applicable
287            if field_name == "project name" {
288                self.validate_project_name(trimmed)?;
289            }
290            Ok(trimmed.to_string())
291        }
292    }
293
294    /// Validate project name according to requirements
295    fn validate_project_name(&self, name: &str) -> Result<()> {
296        // Check if name is valid: alphanumeric characters, hyphens, and underscores only
297        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        // Check for other common invalid patterns
308        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    /// Initialize using cargo init, then enhance for Actor-RTC
330    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        // Step 1: Run cargo init - let it handle all validation
339        let mut cmd = std::process::Command::new("cargo");
340        cmd.arg("init").arg("--quiet").current_dir(project_dir);
341
342        // Add explicit name if provided (for new directories)
343        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        // Step 2: Read the project name that cargo determined
359        let project_name = self.extract_project_name_from_cargo_toml(project_dir)?;
360        info!("📦 Rust project initialized: '{}'", project_name);
361
362        // Step 3: Enhance with Actor-RTC specific files
363        self.enhance_cargo_project_for_actr(project_dir, &project_name, signaling_url)?;
364
365        Ok(())
366    }
367
368    /// Extract project name from Cargo.toml generated by cargo init
369    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        // Parse TOML to extract project name
374        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        // Fallback to directory name if parsing fails
384        Ok("actor-service".to_string())
385    }
386
387    /// Enhance cargo-generated project with Actor-RTC specific features
388    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        // Create proto directory
397        let proto_dir = project_dir.join("proto");
398        std::fs::create_dir_all(&proto_dir)?;
399        info!("📁 Created proto/ directory");
400
401        // Generate Actr.toml
402        self.create_actr_config(project_dir, project_name, signaling_url)?;
403        info!("📄 Created Actr.toml configuration");
404
405        // Enhance Cargo.toml with Actor-RTC dependencies
406        self.enhance_cargo_toml_with_actr_deps(project_dir)?;
407        info!("📦 Enhanced Cargo.toml with Actor-RTC dependencies");
408
409        // Create .gitignore if it doesn't exist
410        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    /// Add Actor-RTC dependencies to existing Cargo.toml
420    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        // Add Actor-RTC dependencies if not already present
426        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}