1use crate::commands::Command;
9use crate::commands::SupportedLanguage;
10use crate::commands::codegen::{GenContext, execute_codegen};
11use crate::error::{ActrCliError, Result};
12use async_trait::async_trait;
15use clap::Args;
16use std::path::{Path, PathBuf};
17use std::process::Command as StdCommand;
18use tracing::{debug, info, warn};
19
20#[derive(Args, Debug, Clone)]
21#[command(
22 about = "Generate code from proto files",
23 long_about = "Generate Actor code in multiple languages from proto files, including protobuf message types, Actor infrastructure code, and user business logic scaffolds"
24)]
25pub struct GenCommand {
26 #[arg(short, long, default_value = "proto")]
28 pub input: PathBuf,
29
30 #[arg(short, long, default_value = "src/generated")]
32 pub output: PathBuf,
33
34 #[arg(short, long, default_value = "Actr.toml")]
36 pub config: PathBuf,
37
38 #[arg(long = "clean")]
40 pub clean: bool,
41
42 #[arg(long = "no-scaffold")]
44 pub no_scaffold: bool,
45
46 #[arg(long)]
48 pub overwrite_user_code: bool,
49
50 #[arg(long = "no-format")]
52 pub no_format: bool,
53
54 #[arg(long)]
56 pub debug: bool,
57
58 #[arg(short, long, default_value = "rust")]
60 pub language: SupportedLanguage,
61}
62
63#[async_trait]
64impl Command for GenCommand {
65 async fn execute(&self) -> Result<()> {
66 info!(
67 "🚀 Start code generation (language: {:?})...",
68 self.language
69 );
70 let config = actr_config::ConfigParser::from_file(&self.config)
71 .map_err(|e| ActrCliError::config_error(format!("Failed to parse Actr.toml: {e}")))?;
72
73 let proto_files = self.preprocess()?;
74 if self.language != SupportedLanguage::Rust {
75 let context = GenContext {
76 proto_files,
77 input_path: self.input.clone(),
78 output: self.output.clone(),
79 config: config.clone(),
80 no_scaffold: self.no_scaffold,
81 overwrite_user_code: self.overwrite_user_code,
82 no_format: self.no_format,
83 debug: self.debug,
84 };
85 execute_codegen(self.language, &context).await?;
86 return Ok(());
87 }
88
89 self.generate_infrastructure_code(&proto_files, &config)
91 .await?;
92
93 if self.should_generate_scaffold() {
95 self.generate_user_code_scaffold(&proto_files).await?;
96 }
97
98 if self.should_format() {
100 self.format_generated_code().await?;
101 }
102
103 self.validate_generated_code().await?;
105
106 info!("✅ Code generation completed!");
107 self.set_generated_files_readonly()?;
109 self.print_next_steps();
110
111 Ok(())
112 }
113}
114
115impl GenCommand {
116 fn preprocess(&self) -> Result<Vec<PathBuf>> {
117 self.validate_inputs()?;
119
120 self.clean_generated_outputs()?;
122
123 self.prepare_output_dirs()?;
125
126 let proto_files = self.discover_proto_files()?;
128 info!("📁 Found {} proto files", proto_files.len());
129
130 Ok(proto_files)
131 }
132
133 fn should_generate_scaffold(&self) -> bool {
135 !self.no_scaffold
136 }
137
138 fn should_format(&self) -> bool {
140 !self.no_format
141 }
142
143 fn clean_generated_outputs(&self) -> Result<()> {
145 use std::fs;
146
147 if !self.clean {
148 return Ok(());
149 }
150
151 if !self.output.exists() {
152 return Ok(());
153 }
154
155 info!("🧹 Cleaning old generation results: {:?}", self.output);
156
157 self.make_writable_recursive(&self.output)?;
158 fs::remove_dir_all(&self.output).map_err(|e| {
159 ActrCliError::config_error(format!("Failed to delete generation directory: {e}"))
160 })?;
161
162 Ok(())
163 }
164
165 #[allow(clippy::only_used_in_recursion)]
167 fn make_writable_recursive(&self, path: &Path) -> Result<()> {
168 use std::fs;
169
170 if path.is_file() {
171 let metadata = fs::metadata(path).map_err(|e| {
172 ActrCliError::config_error(format!("Failed to read file metadata: {e}"))
173 })?;
174 let mut permissions = metadata.permissions();
175
176 #[cfg(unix)]
177 {
178 use std::os::unix::fs::PermissionsExt;
179 let mode = permissions.mode();
180 permissions.set_mode(mode | 0o222);
181 }
182
183 #[cfg(not(unix))]
184 {
185 permissions.set_readonly(false);
186 }
187
188 fs::set_permissions(path, permissions).map_err(|e| {
189 ActrCliError::config_error(format!("Failed to reset file permissions: {e}"))
190 })?;
191 } else if path.is_dir() {
192 for entry in fs::read_dir(path)
193 .map_err(|e| ActrCliError::config_error(format!("Failed to read directory: {e}")))?
194 {
195 let entry = entry.map_err(|e| ActrCliError::config_error(e.to_string()))?;
196 self.make_writable_recursive(&entry.path())?;
197 }
198 }
199
200 Ok(())
201 }
202
203 fn validate_inputs(&self) -> Result<()> {
205 if !self.input.exists() {
206 return Err(ActrCliError::config_error(format!(
207 "Input path does not exist: {:?}",
208 self.input
209 )));
210 }
211
212 if self.input.is_file() && self.input.extension().unwrap_or_default() != "proto" {
213 warn!("Input file is not a .proto file: {:?}", self.input);
214 }
215
216 Ok(())
217 }
218
219 fn prepare_output_dirs(&self) -> Result<()> {
221 std::fs::create_dir_all(&self.output).map_err(|e| {
222 ActrCliError::config_error(format!("Failed to create output directory: {e}"))
223 })?;
224
225 if self.should_generate_scaffold() {
226 let user_code_dir = self.output.join("../");
227 std::fs::create_dir_all(&user_code_dir).map_err(|e| {
228 ActrCliError::config_error(format!("Failed to create user code directory: {e}"))
229 })?;
230 }
231
232 Ok(())
233 }
234
235 fn discover_proto_files(&self) -> Result<Vec<PathBuf>> {
237 let mut proto_files = Vec::new();
238
239 if self.input.is_file() {
240 proto_files.push(self.input.clone());
241 } else {
242 for entry in std::fs::read_dir(&self.input).map_err(|e| {
244 ActrCliError::config_error(format!("Failed to read input directory: {e}"))
245 })? {
246 let entry = entry.map_err(|e| ActrCliError::config_error(e.to_string()))?;
247 let path = entry.path();
248
249 if path.extension().unwrap_or_default() == "proto" {
250 proto_files.push(path);
251 }
252 }
253 }
254
255 if proto_files.is_empty() {
256 return Err(ActrCliError::config_error("No proto files found"));
257 }
258
259 Ok(proto_files)
260 }
261
262 fn ensure_protoc_plugin(&self) -> Result<PathBuf> {
274 const EXPECTED_VERSION: &str = env!("CARGO_PKG_VERSION");
276
277 let installed_version = self.check_installed_plugin_version()?;
279
280 match installed_version {
281 Some(version) if version == EXPECTED_VERSION => {
282 info!("✅ Using installed protoc-gen-actrframework v{}", version);
284 let output = StdCommand::new("which")
285 .arg("protoc-gen-actrframework")
286 .output()
287 .map_err(|e| {
288 ActrCliError::command_error(format!("Failed to locate plugin: {e}"))
289 })?;
290
291 let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
292 Ok(PathBuf::from(path))
293 }
294 Some(version) => {
295 info!(
297 "🔄 Version mismatch: installed v{}, need v{}",
298 version, EXPECTED_VERSION
299 );
300 info!("🔨 Upgrading plugin...");
301 self.install_or_upgrade_plugin()
302 }
303 None => {
304 info!("📦 protoc-gen-actrframework not found, installing...");
306 self.install_or_upgrade_plugin()
307 }
308 }
309 }
310
311 fn check_installed_plugin_version(&self) -> Result<Option<String>> {
313 let output = StdCommand::new("protoc-gen-actrframework")
314 .arg("--version")
315 .output();
316
317 match output {
318 Ok(output) if output.status.success() => {
319 let version_info = String::from_utf8_lossy(&output.stdout);
320 let version = version_info
322 .lines()
323 .next()
324 .and_then(|line| line.split_whitespace().nth(1))
325 .map(|v| v.to_string());
326
327 debug!("Detected installed version: {:?}", version);
328 Ok(version)
329 }
330 _ => {
331 debug!("Plugin not found in PATH");
332 Ok(None)
333 }
334 }
335 }
336
337 fn install_or_upgrade_plugin(&self) -> Result<PathBuf> {
339 let current_dir = std::env::current_dir()?;
341 let workspace_root = current_dir.ancestors().find(|p| {
342 let is_workspace =
343 p.join("Cargo.toml").exists() && p.join("crates/framework-protoc-codegen").exists();
344 if is_workspace {
345 debug!("Found workspace root: {:?}", p);
346 }
347 is_workspace
348 });
349
350 let workspace_root = workspace_root.ok_or_else(|| {
351 ActrCliError::config_error(
352 "Cannot find actr workspace.\n\
353 Please run this command from within an actr project or workspace.",
354 )
355 })?;
356
357 info!("🔍 Found actr workspace at: {}", workspace_root.display());
358
359 info!("🔨 Building protoc-gen-actrframework...");
361 let mut build_cmd = StdCommand::new("cargo");
362 build_cmd
363 .arg("build")
364 .arg("-p")
365 .arg("actr-framework-protoc-codegen")
366 .arg("--bin")
367 .arg("protoc-gen-actrframework")
368 .current_dir(workspace_root);
369
370 debug!("Running: {:?}", build_cmd);
371 let output = build_cmd
372 .output()
373 .map_err(|e| ActrCliError::command_error(format!("Failed to build plugin: {e}")))?;
374
375 if !output.status.success() {
376 let stderr = String::from_utf8_lossy(&output.stderr);
377 return Err(ActrCliError::command_error(format!(
378 "Failed to build plugin:\n{stderr}"
379 )));
380 }
381
382 info!("📦 Installing to ~/.cargo/bin/...");
384 let mut install_cmd = StdCommand::new("cargo");
385 install_cmd
386 .arg("install")
387 .arg("--path")
388 .arg(workspace_root.join("crates/framework-protoc-codegen"))
389 .arg("--bin")
390 .arg("protoc-gen-actrframework")
391 .arg("--force"); debug!("Running: {:?}", install_cmd);
394 let output = install_cmd
395 .output()
396 .map_err(|e| ActrCliError::command_error(format!("Failed to install plugin: {e}")))?;
397
398 if !output.status.success() {
399 let stderr = String::from_utf8_lossy(&output.stderr);
400 return Err(ActrCliError::command_error(format!(
401 "Failed to install plugin:\n{stderr}"
402 )));
403 }
404
405 info!("✅ Plugin installed successfully");
406
407 let which_output = StdCommand::new("which")
409 .arg("protoc-gen-actrframework")
410 .output()
411 .map_err(|e| {
412 ActrCliError::command_error(format!("Failed to locate installed plugin: {e}"))
413 })?;
414
415 let path = String::from_utf8_lossy(&which_output.stdout)
416 .trim()
417 .to_string();
418 Ok(PathBuf::from(path))
419 }
420
421 async fn generate_infrastructure_code(
423 &self,
424 proto_files: &[PathBuf],
425 config: &actr_config::Config,
426 ) -> Result<()> {
427 info!("🔧 Generating infrastructure code...");
428
429 let plugin_path = self.ensure_protoc_plugin()?;
431
432 let manufacturer = config.package.actr_type.manufacturer.clone();
433 debug!("Using manufacturer from Actr.toml: {}", manufacturer);
434
435 for proto_file in proto_files {
436 debug!("Processing proto file: {:?}", proto_file);
437
438 let mut cmd = StdCommand::new("protoc");
440 cmd.arg(format!("--proto_path={}", self.input.display()))
441 .arg("--prost_opt=flat_output_dir")
442 .arg(format!("--prost_out={}", self.output.display()))
443 .arg(proto_file);
444
445 debug!("Executing protoc (prost): {:?}", cmd);
446 let output = cmd.output().map_err(|e| {
447 ActrCliError::command_error(format!("Failed to execute protoc (prost): {e}"))
448 })?;
449
450 if !output.status.success() {
451 let stderr = String::from_utf8_lossy(&output.stderr);
452 return Err(ActrCliError::command_error(format!(
453 "protoc (prost) execution failed: {stderr}"
454 )));
455 }
456
457 let mut cmd = StdCommand::new("protoc");
459 cmd.arg(format!("--proto_path={}", self.input.display()))
460 .arg(format!(
461 "--plugin=protoc-gen-actrframework={}",
462 plugin_path.display()
463 ))
464 .arg(format!("--actrframework_opt=manufacturer={manufacturer}"))
465 .arg(format!("--actrframework_out={}", self.output.display()))
466 .arg(proto_file);
467
468 debug!("Executing protoc (actrframework): {:?}", cmd);
469 let output = cmd.output().map_err(|e| {
470 ActrCliError::command_error(format!(
471 "Failed to execute protoc (actrframework): {e}"
472 ))
473 })?;
474
475 if !output.status.success() {
476 let stderr = String::from_utf8_lossy(&output.stderr);
477 return Err(ActrCliError::command_error(format!(
478 "protoc (actrframework) execution failed: {stderr}"
479 )));
480 }
481
482 let stdout = String::from_utf8_lossy(&output.stdout);
483 if !stdout.is_empty() {
484 debug!("protoc output: {}", stdout);
485 }
486 }
487
488 self.generate_mod_rs(proto_files).await?;
490
491 info!("✅ Infrastructure code generation completed");
492 Ok(())
493 }
494
495 async fn generate_mod_rs(&self, _proto_files: &[PathBuf]) -> Result<()> {
497 let mod_path = self.output.join("mod.rs");
498
499 let mut proto_modules = Vec::new();
501 let mut service_modules = Vec::new();
502
503 use std::fs;
504 for entry in fs::read_dir(&self.output).map_err(|e| {
505 ActrCliError::config_error(format!("Failed to read output directory: {e}"))
506 })? {
507 let entry = entry.map_err(|e| ActrCliError::config_error(e.to_string()))?;
508 let path = entry.path();
509
510 if path.is_file()
511 && path.extension().unwrap_or_default() == "rs"
512 && let Some(file_name) = path.file_stem().and_then(|s| s.to_str())
513 {
514 if file_name == "mod" {
516 continue;
517 }
518
519 if file_name.ends_with("_service_actor") {
521 service_modules.push(format!("pub mod {file_name};"));
522 } else {
523 proto_modules.push(format!("pub mod {file_name};"));
524 }
525 }
526 }
527
528 proto_modules.sort();
530 service_modules.sort();
531
532 let mod_content = format!(
533 r#"//! Automatically generated code module
534//!
535//! This module is automatically generated by the `actr gen` command, including:
536//! - protobuf message type definitions
537//! - Actor framework code (router, traits)
538//!
539//! ⚠️ Do not manually modify files in this directory
540
541// Protobuf message types (generated by prost)
542{}
543
544// Actor framework code (generated by protoc-gen-actrframework)
545{}
546
547// Common types are defined in their respective modules, please import as needed
548"#,
549 proto_modules.join("\n"),
550 service_modules.join("\n"),
551 );
552
553 std::fs::write(&mod_path, mod_content)
554 .map_err(|e| ActrCliError::config_error(format!("Failed to write mod.rs: {e}")))?;
555
556 debug!("Generated mod.rs: {:?}", mod_path);
557 Ok(())
558 }
559
560 fn set_generated_files_readonly(&self) -> Result<()> {
562 use std::fs;
563
564 for entry in fs::read_dir(&self.output).map_err(|e| {
565 ActrCliError::config_error(format!("Failed to read output directory: {e}"))
566 })? {
567 let entry = entry.map_err(|e| ActrCliError::config_error(e.to_string()))?;
568 let path = entry.path();
569
570 if path.is_file() && path.extension().unwrap_or_default() == "rs" {
571 let metadata = fs::metadata(&path).map_err(|e| {
573 ActrCliError::config_error(format!("Failed to get file metadata: {e}"))
574 })?;
575 let mut permissions = metadata.permissions();
576
577 #[cfg(unix)]
579 {
580 use std::os::unix::fs::PermissionsExt;
581 let mode = permissions.mode();
582 permissions.set_mode(mode & !0o222); }
584
585 #[cfg(not(unix))]
586 {
587 permissions.set_readonly(true);
588 }
589
590 fs::set_permissions(&path, permissions).map_err(|e| {
591 ActrCliError::config_error(format!("Failed to set file permissions: {e}"))
592 })?;
593
594 debug!("Set read-only attribute: {:?}", path);
595 }
596 }
597
598 Ok(())
599 }
600
601 async fn generate_user_code_scaffold(&self, proto_files: &[PathBuf]) -> Result<()> {
603 info!("📝 Generating user code scaffold...");
604
605 for proto_file in proto_files {
606 let service_name = proto_file
607 .file_stem()
608 .and_then(|s| s.to_str())
609 .ok_or_else(|| ActrCliError::config_error("Invalid proto file name"))?;
610
611 self.generate_service_scaffold(service_name).await?;
612 }
613
614 info!("✅ User code scaffold generation completed");
615 Ok(())
616 }
617
618 async fn generate_service_scaffold(&self, service_name: &str) -> Result<()> {
620 let user_file_path = self
621 .output
622 .parent()
623 .unwrap_or_else(|| Path::new("src"))
624 .join(format!("{}_service.rs", service_name.to_lowercase()));
625
626 if user_file_path.exists() && !self.overwrite_user_code {
628 info!("⏭️ Skipping existing user code file: {:?}", user_file_path);
629 return Ok(());
630 }
631
632 let scaffold_content = self.generate_scaffold_content(service_name);
633
634 std::fs::write(&user_file_path, scaffold_content).map_err(|e| {
635 ActrCliError::config_error(format!("Failed to write user code scaffold: {e}"))
636 })?;
637
638 info!("📄 Generated user code scaffold: {:?}", user_file_path);
639 Ok(())
640 }
641
642 fn generate_scaffold_content(&self, service_name: &str) -> String {
644 let service_name_pascal = service_name
645 .split('_')
646 .map(|s| {
647 let mut chars = s.chars();
648 match chars.next() {
649 None => String::new(),
650 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
651 }
652 })
653 .collect::<String>();
654
655 let template = format!(
656 r#"//! # {service_name_pascal} user business logic implementation
657//!
658//! This file is a user code scaffold automatically generated by the `actr gen` command.
659//! Please implement your specific business logic here.
660
661use crate::generated::{{{service_name_pascal}Handler, {service_name_pascal}Actor}};
662// 只导入必要的类型,避免拉入不需要的依赖如 sqlite
663// use actr_framework::prelude::*;
664use std::sync::Arc;
665
666/// Specific implementation of the {service_name_pascal} service
667///
668/// TODO: Add state fields you need, for example:
669/// - Database connection pool
670/// - Configuration information
671/// - Cache client
672/// - Logger, etc.
673pub struct My{service_name_pascal}Service {{
674 // TODO: Add your service state fields
675 // For example:
676 // pub db_pool: Arc<DatabasePool>,
677 // pub config: Arc<ServiceConfig>,
678 // pub metrics: Arc<Metrics>,
679}}
680
681impl My{service_name_pascal}Service {{
682 /// Create a new service instance
683 ///
684 /// TODO: Modify constructor parameters as needed
685 pub fn new(/* TODO: Add necessary dependencies */) -> Self {{
686 Self {{
687 // TODO: Initialize your fields
688 }}
689 }}
690
691 /// Create a service instance with default configuration (for testing)
692 pub fn default_for_testing() -> Self {{
693 Self {{
694 // TODO: Provide default values for testing
695 }}
696 }}
697}}
698
699// TODO: Implement all methods of the {service_name_pascal}Handler trait
700// Note: The impl_user_code_scaffold! macro has generated a basic scaffold for you,
701// you need to replace it with real business logic implementation.
702//
703// Example:
704// #[async_trait]
705// impl {service_name_pascal}Handler for My{service_name_pascal}Service {{
706// async fn method_name(&self, req: RequestType) -> ActorResult<ResponseType> {{
707// // 1. Validate input
708// // 2. Execute business logic
709// // 3. Return result
710// todo!("Implement your business logic")
711// }}
712// }}
713
714#[cfg(test)]
715mod tests {{
716 use super::*;
717
718 #[tokio::test]
719 async fn test_service_creation() {{
720 let _service = My{service_name_pascal}Service::default_for_testing();
721 // TODO: Add your tests
722 }}
723
724 // TODO: Add more test cases
725}}
726
727/*
728📚 User Guide
729
730## 🚀 Quick Start
731
7321. **Implement business logic**:
733 Implement all methods of the `{service_name_pascal}Handler` trait in `My{service_name_pascal}Service`
734
7352. **Add dependencies**:
736 Add dependencies you need in `Cargo.toml`, such as database clients, HTTP clients, etc.
737
7383. **Configure service**:
739 Modify the `new()` constructor to inject necessary dependencies
740
7414. **Start service**:
742 ```rust
743 #[tokio::main]
744 async fn main() -> ActorResult<()> {{
745 let service = My{service_name_pascal}Service::new(/* dependencies */);
746
747 ActorSystem::new()
748 .attach(service)
749 .start()
750 .await
751 }}
752 ```
753
754## 🔧 Development Tips
755
756- Use `tracing` crate for logging
757- Implement error handling and retry logic
758- Add unit and integration tests
759- Consider using configuration files for environment variables
760- Implement health checks and metrics collection
761
762## 📖 More Resources
763
764- Actor-RTC Documentation: [Link]
765- API Reference: [Link]
766- Example Projects: [Link]
767*/
768"# );
770
771 template
772 }
773
774 async fn format_generated_code(&self) -> Result<()> {
776 info!("🎨 Formatting generated code...");
777
778 let mut cmd = StdCommand::new("rustfmt");
779 cmd.arg("--edition")
780 .arg("2024")
781 .arg("--config")
782 .arg("max_width=100");
783
784 for entry in std::fs::read_dir(&self.output).map_err(|e| {
786 ActrCliError::config_error(format!("Failed to read output directory: {e}"))
787 })? {
788 let entry = entry.map_err(|e| ActrCliError::config_error(e.to_string()))?;
789 let path = entry.path();
790
791 if path.extension().unwrap_or_default() == "rs" {
792 cmd.arg(&path);
793 }
794 }
795
796 let output = cmd
797 .output()
798 .map_err(|e| ActrCliError::command_error(format!("Failed to execute rustfmt: {e}")))?;
799
800 if !output.status.success() {
801 let stderr = String::from_utf8_lossy(&output.stderr);
802 warn!("rustfmt execution warning: {}", stderr);
803 } else {
804 info!("✅ Code formatting completed");
805 }
806
807 Ok(())
808 }
809
810 async fn validate_generated_code(&self) -> Result<()> {
812 info!("🔍 Validating generated code...");
813
814 let project_root = self.find_project_root()?;
816
817 let mut cmd = StdCommand::new("cargo");
818 cmd.arg("check").arg("--quiet").current_dir(&project_root);
819
820 let output = cmd.output().map_err(|e| {
821 ActrCliError::command_error(format!("Failed to execute cargo check: {e}"))
822 })?;
823
824 if !output.status.success() {
825 let stderr = String::from_utf8_lossy(&output.stderr);
826 warn!(
827 "Generated code has compilation warnings or errors:\n{}",
828 stderr
829 );
830 info!("💡 This is usually normal because the user code scaffold contains TODO markers");
831 } else {
832 info!("✅ Code validation passed");
833 }
834
835 Ok(())
836 }
837
838 fn find_project_root(&self) -> Result<PathBuf> {
840 let mut current = std::env::current_dir().map_err(ActrCliError::Io)?;
841
842 loop {
843 if current.join("Cargo.toml").exists() {
844 return Ok(current);
845 }
846
847 match current.parent() {
848 Some(parent) => current = parent.to_path_buf(),
849 None => break,
850 }
851 }
852
853 std::env::current_dir().map_err(ActrCliError::Io)
855 }
856
857 fn print_next_steps(&self) {
859 println!("\n🎉 Code generation completed!");
860 println!("\n📋 Next steps:");
861 println!("1. 📖 View generated code: {:?}", self.output);
862 if self.should_generate_scaffold() {
863 println!(
864 "2. ✏️ Implement business logic: in the *_service.rs files in the src/ directory"
865 );
866 println!("3. 🔧 Add dependencies: add required packages in Cargo.toml");
867 println!("4. 🏗️ Build project: cargo build");
868 println!("5. 🧪 Run tests: cargo test");
869 println!("6. 🚀 Start service: cargo run");
870 } else {
871 println!("2. 🏗️ Build project: cargo build");
872 println!("3. 🧪 Run tests: cargo test");
873 println!("4. 🚀 Start service: cargo run");
874 }
875 println!("\n💡 Tip: Check the detailed user guide in the generated user code files");
876 }
877}