Skip to main content

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, data-stream)
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        let context = InitContext {
73            project_dir: project_dir.clone(),
74            project_name: project_name.clone(),
75            signaling_url: signaling_url.clone(),
76            template: self.template,
77            is_current_dir: project_dir == Path::new("."),
78        };
79
80        initialize::execute_initialize(self.language, &context).await?;
81
82        Ok(())
83    }
84}
85
86impl InitCommand {
87    fn resolve_project_info(&self, name: &str) -> Result<(PathBuf, String)> {
88        if name == "." {
89            // Initialize in current directory - name will be inferred
90            let project_name = if let Some(name) = &self.project_name {
91                name.clone()
92            } else {
93                let current_dir = std::env::current_dir().map_err(|e| {
94                    ActrCliError::InvalidProject(format!(
95                        "Failed to resolve current directory: {e}"
96                    ))
97                })?;
98                current_dir
99                    .file_name()
100                    .and_then(|s| s.to_str())
101                    .map(|s| s.to_string())
102                    .ok_or_else(|| {
103                        ActrCliError::InvalidProject(
104                            "Failed to infer project name from current directory".to_string(),
105                        )
106                    })?
107            };
108            Ok((PathBuf::from("."), project_name))
109        } else {
110            // Create new directory - extract project name from path
111            let path = PathBuf::from(name);
112            let project_name = path
113                .file_name()
114                .and_then(|s| s.to_str())
115                .unwrap_or(name)
116                .to_string();
117            Ok((path, project_name))
118        }
119    }
120
121    /// Interactive prompt for missing fields with detailed guidance
122    fn prompt_if_missing(
123        &self,
124        field_name: &str,
125        current_value: Option<&String>,
126    ) -> Result<String> {
127        if let Some(value) = current_value {
128            return Ok(value.clone());
129        }
130
131        match field_name {
132            "project name" => {
133                println!("┌──────────────────────────────────────────────────────────┐");
134                println!("│ 📋  Project Name Configuration                           │");
135                println!("├──────────────────────────────────────────────────────────┤");
136                println!("│                                                          │");
137                println!("│  📝 Requirements:                                        │");
138                println!("│     • Only alphanumeric characters, hyphens and _        │");
139                println!("│     • Cannot start or end with - or _                    │");
140                println!("│                                                          │");
141                println!("│  💡 Examples:                                            │");
142                println!("│     my-chat-service, user-manager, media_streamer        │");
143                println!("│                                                          │");
144                println!("└──────────────────────────────────────────────────────────┘");
145                print!("🎯 Enter project name [my-actor-project]: ");
146            }
147            "signaling server URL" => {
148                println!("┌──────────────────────────────────────────────────────────┐");
149                println!("│ 🌐  Signaling Server Configuration                       │");
150                println!("├──────────────────────────────────────────────────────────┤");
151                println!("│                                                          │");
152                println!("│  📡 WebSocket URL for Actor-RTC signaling coordination   │");
153                println!("│                                                          │");
154                println!("│  💡 Examples:                                            │");
155                println!("│     ws://localhost:8080/                (development)    │");
156                println!("│     wss://example.com                   (production      │");
157                println!("│     wss://example.com/?token=${{TOKEN}}   (with auth)    │");
158                println!("│                                                          │");
159                println!("└──────────────────────────────────────────────────────────┘");
160                print!("🎯 Enter signaling server URL [wss://actrix1.develenv.com]: ");
161            }
162            _ => {
163                print!("🎯 Enter {field_name}: ");
164            }
165        }
166
167        io::stdout().flush().map_err(ActrCliError::Io)?;
168
169        let mut input = String::new();
170        io::stdin()
171            .read_line(&mut input)
172            .map_err(ActrCliError::Io)?;
173
174        println!();
175
176        let trimmed = input.trim();
177        if trimmed.is_empty() {
178            // Provide sensible defaults
179            let default = match field_name {
180                "project name" => "my-actor-project",
181                "signaling server URL" => "wss://actrix1.develenv.com/signaling/ws",
182                _ => {
183                    return Err(ActrCliError::InvalidProject(format!(
184                        "{field_name} cannot be empty"
185                    )));
186                }
187            };
188            Ok(default.to_string())
189        } else {
190            // Validate project name if applicable
191            if field_name == "project name" {
192                self.validate_project_name(trimmed)?;
193            }
194            Ok(trimmed.to_string())
195        }
196    }
197
198    /// Validate project name according to requirements
199    fn validate_project_name(&self, name: &str) -> Result<()> {
200        // Check if name is valid: alphanumeric characters, hyphens, and underscores only
201        let is_valid = name
202            .chars()
203            .all(|c| c.is_alphanumeric() || c == '-' || c == '_');
204
205        if !is_valid {
206            return Err(ActrCliError::InvalidProject(format!(
207                "Invalid project name '{name}'. Only alphanumeric characters, hyphens, and underscores are allowed."
208            )));
209        }
210
211        // Check for other common invalid patterns
212        if name.is_empty() {
213            return Err(ActrCliError::InvalidProject(
214                "Project name cannot be empty".to_string(),
215            ));
216        }
217
218        if name.starts_with('-') || name.ends_with('-') {
219            return Err(ActrCliError::InvalidProject(
220                "Project name cannot start or end with a hyphen".to_string(),
221            ));
222        }
223
224        if name.starts_with('_') || name.ends_with('_') {
225            return Err(ActrCliError::InvalidProject(
226                "Project name cannot start or end with an underscore".to_string(),
227            ));
228        }
229
230        Ok(())
231    }
232}