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