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 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 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 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 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 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 if field_name == "project name" {
192 self.validate_project_name(trimmed)?;
193 }
194 Ok(trimmed.to_string())
195 }
196 }
197
198 fn validate_project_name(&self, name: &str) -> Result<()> {
200 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 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}