actr_cli/commands/
generate.rs

1//! # Code Generation Command
2//!
3//! Generate Rust Actor code from proto files, including:
4//! 1. Protobuf message types
5//! 2. Actor infrastructure code
6//! 3. User business logic scaffolds (with TODO comments)
7
8use crate::commands::Command;
9use crate::commands::SupportedLanguage;
10use crate::commands::codegen::{GenContext, execute_codegen};
11use crate::error::{ActrCliError, Result};
12use crate::plugin_config::{load_protoc_plugin_config, version_is_at_least};
13use crate::utils::to_pascal_case;
14// 只导入必要的类型,避免拉入不需要的依赖如 sqlite
15// use actr_framework::prelude::*;
16use async_trait::async_trait;
17use clap::Args;
18use std::path::{Path, PathBuf};
19use std::process::Command as StdCommand;
20use tracing::{debug, info, warn};
21
22#[derive(Args, Debug, Clone)]
23#[command(
24    about = "Generate code from proto files",
25    after_help = "Default output paths by language:
26  - rust:   src/generated
27  - swift:  {PascalName}/Generated (e.g., EchoApp/Generated)
28  - kotlin: app/src/main/java/{package}/generated
29  - python: generated"
30)]
31pub struct GenCommand {
32    /// Input proto file or directory
33    #[arg(short, long, default_value = "protos")]
34    pub input: PathBuf,
35
36    /// Output directory for generated code (use -o to override language defaults)
37    #[arg(short, long)]
38    pub output: Option<PathBuf>,
39
40    /// Path to Actr.toml config file
41    #[arg(short, long, default_value = "Actr.toml")]
42    pub config: PathBuf,
43
44    /// Clean generated outputs before regenerating
45    #[arg(long = "clean")]
46    pub clean: bool,
47
48    /// Skip user code scaffold generation
49    #[arg(long = "no-scaffold")]
50    pub no_scaffold: bool,
51
52    /// Whether to overwrite existing user code files
53    #[arg(long)]
54    pub overwrite_user_code: bool,
55
56    /// Skip formatting
57    #[arg(long = "no-format")]
58    pub no_format: bool,
59
60    /// Debug mode: keep intermediate generated files
61    #[arg(long)]
62    pub debug: bool,
63
64    /// Target language for generation
65    #[arg(short, long, default_value = "rust")]
66    pub language: SupportedLanguage,
67}
68
69#[async_trait]
70impl Command for GenCommand {
71    async fn execute(&self) -> Result<()> {
72        // Check if Actr.lock.toml exists
73        self.check_lock_file()?;
74
75        // Determine output path based on language
76        let output = self.determine_output_path()?;
77
78        info!(
79            "🚀 Start code generation (language: {:?})...",
80            self.language
81        );
82        let config = actr_config::ConfigParser::from_file(&self.config)
83            .map_err(|e| ActrCliError::config_error(format!("Failed to parse Actr.toml: {e}")))?;
84
85        let proto_files = self.preprocess()?;
86        if self.language != SupportedLanguage::Rust {
87            let context = GenContext {
88                proto_files,
89                input_path: self.input.clone(),
90                output,
91                config_path: self.config.clone(),
92                config: config.clone(),
93                no_scaffold: self.no_scaffold,
94                overwrite_user_code: self.overwrite_user_code,
95                no_format: self.no_format,
96                debug: self.debug,
97            };
98            execute_codegen(self.language, &context).await?;
99            return Ok(());
100        }
101
102        // Step 5: Generate infrastructure code
103        self.generate_infrastructure_code(&proto_files, &config)
104            .await?;
105
106        // Step 6: Generate user code scaffold
107        if self.should_generate_scaffold() {
108            self.generate_user_code_scaffold(&proto_files).await?;
109        }
110
111        // Step 7: Format code
112        if self.should_format() {
113            self.format_generated_code().await?;
114        }
115
116        // Step 8: Validate generated code
117        self.validate_generated_code().await?;
118
119        info!("✅ Code generation completed!");
120        // Set all generated files to read-only only after generation, formatting, and validation are complete, to not interfere with rustfmt or other steps.
121        self.set_generated_files_readonly()?;
122        self.print_next_steps();
123
124        Ok(())
125    }
126}
127
128impl GenCommand {
129    /// Check if Actr.lock.toml exists and provide helpful error message if not
130    fn check_lock_file(&self) -> Result<()> {
131        let config_dir = self
132            .config
133            .parent()
134            .unwrap_or_else(|| std::path::Path::new("."));
135        let lock_file_path = config_dir.join("Actr.lock.toml");
136
137        if !lock_file_path.exists() {
138            return Err(ActrCliError::config_error(
139                "Actr.lock.toml not found\n\n\
140                The lock file is required for code generation. Please run:\n\n\
141                \x20\x20\x20\x20actr install\n\n\
142                This will generate Actr.lock.toml based on your Actr.toml configuration.",
143            ));
144        }
145
146        Ok(())
147    }
148
149    /// Determine output path based on language if not explicitly specified
150    fn determine_output_path(&self) -> Result<PathBuf> {
151        // If user specified a custom output, use it
152        if let Some(ref output) = self.output {
153            return Ok(output.clone());
154        }
155
156        // Determine language-specific default output path
157        match self.language {
158            SupportedLanguage::Swift => {
159                // Read package name from config for Swift
160                let config = actr_config::ConfigParser::from_file(&self.config).map_err(|e| {
161                    ActrCliError::config_error(format!("Failed to parse Actr.toml: {e}"))
162                })?;
163                let project_name = &config.package.name;
164                // Convert to PascalCase for Swift module name
165                let pascal_name = to_pascal_case(project_name);
166                Ok(PathBuf::from(format!("{}/Generated", pascal_name)))
167            }
168            SupportedLanguage::Kotlin => {
169                // Kotlin default: app/src/main/java/{package_path}/generated
170                // Package name follows the pattern: io.actr.{project_name_cleaned}
171                let config = actr_config::ConfigParser::from_file(&self.config).map_err(|e| {
172                    ActrCliError::config_error(format!("Failed to parse Actr.toml: {e}"))
173                })?;
174                // Convert project name to valid Android package name
175                // e.g., "my-app" -> "io.actr.myapp"
176                let clean_name: String = config
177                    .package
178                    .name
179                    .chars()
180                    .filter(|c| c.is_alphanumeric())
181                    .collect::<String>()
182                    .to_lowercase();
183                let package_path = format!("io/actr/{}", clean_name);
184                Ok(PathBuf::from(format!(
185                    "app/src/main/java/{}/generated",
186                    package_path
187                )))
188            }
189            SupportedLanguage::Python => {
190                // Python default: generated
191                Ok(PathBuf::from("generated"))
192            }
193            SupportedLanguage::Rust => {
194                // Rust default: src/generated
195                Ok(PathBuf::from("src/generated"))
196            }
197        }
198    }
199
200    fn preprocess(&self) -> Result<Vec<PathBuf>> {
201        // Step 1: Validate inputs
202        self.validate_inputs()?;
203
204        // Step 2: Clean old generation outputs (optional)
205        self.clean_generated_outputs()?;
206
207        // Step 3: Prepare output directories
208        self.prepare_output_dirs()?;
209
210        // Step 4: Discover proto files
211        let proto_files = self.discover_proto_files()?;
212        info!("📁 Found {} proto files", proto_files.len());
213
214        Ok(proto_files)
215    }
216
217    /// Whether user code scaffold should be generated
218    fn should_generate_scaffold(&self) -> bool {
219        !self.no_scaffold
220    }
221
222    /// Whether formatting should run
223    fn should_format(&self) -> bool {
224        !self.no_format
225    }
226
227    /// Remove previously generated files when --clean is used
228    fn clean_generated_outputs(&self) -> Result<()> {
229        use std::fs;
230
231        if !self.clean {
232            return Ok(());
233        }
234
235        let output = self.determine_output_path()?;
236        if !output.exists() {
237            return Ok(());
238        }
239
240        info!("🧹 Cleaning old generation results: {:?}", output);
241
242        self.make_writable_recursive(&output)?;
243        fs::remove_dir_all(&output).map_err(|e| {
244            ActrCliError::config_error(format!("Failed to delete generation directory: {e}"))
245        })?;
246
247        Ok(())
248    }
249
250    /// Ensure all files are writable so removal works across platforms
251    #[allow(clippy::only_used_in_recursion)]
252    fn make_writable_recursive(&self, path: &Path) -> Result<()> {
253        use std::fs;
254
255        if path.is_file() {
256            let metadata = fs::metadata(path).map_err(|e| {
257                ActrCliError::config_error(format!("Failed to read file metadata: {e}"))
258            })?;
259            let mut permissions = metadata.permissions();
260
261            #[cfg(unix)]
262            {
263                use std::os::unix::fs::PermissionsExt;
264                let mode = permissions.mode();
265                permissions.set_mode(mode | 0o222);
266            }
267
268            #[cfg(not(unix))]
269            {
270                permissions.set_readonly(false);
271            }
272
273            fs::set_permissions(path, permissions).map_err(|e| {
274                ActrCliError::config_error(format!("Failed to reset file permissions: {e}"))
275            })?;
276        } else if path.is_dir() {
277            for entry in fs::read_dir(path)
278                .map_err(|e| ActrCliError::config_error(format!("Failed to read directory: {e}")))?
279            {
280                let entry = entry.map_err(|e| ActrCliError::config_error(e.to_string()))?;
281                self.make_writable_recursive(&entry.path())?;
282            }
283        }
284
285        Ok(())
286    }
287
288    /// 验证输入参数
289    fn validate_inputs(&self) -> Result<()> {
290        if !self.input.exists() {
291            return Err(ActrCliError::config_error(format!(
292                "Input path does not exist: {:?}",
293                self.input
294            )));
295        }
296
297        if self.input.is_file() && self.input.extension().unwrap_or_default() != "proto" {
298            warn!("Input file is not a .proto file: {:?}", self.input);
299        }
300
301        Ok(())
302    }
303
304    /// 准备输出目录
305    fn prepare_output_dirs(&self) -> Result<()> {
306        let output = self.determine_output_path()?;
307        std::fs::create_dir_all(&output).map_err(|e| {
308            ActrCliError::config_error(format!("Failed to create output directory: {e}"))
309        })?;
310
311        if self.should_generate_scaffold() {
312            let user_code_dir = output.join("../");
313            std::fs::create_dir_all(&user_code_dir).map_err(|e| {
314                ActrCliError::config_error(format!("Failed to create user code directory: {e}"))
315            })?;
316        }
317
318        Ok(())
319    }
320
321    /// Find proto files recursively
322    fn discover_proto_files(&self) -> Result<Vec<PathBuf>> {
323        let mut proto_files = Vec::new();
324
325        if self.input.is_file() {
326            proto_files.push(self.input.clone());
327        } else {
328            self.collect_proto_files(&self.input, &mut proto_files)?;
329        }
330
331        if proto_files.is_empty() {
332            return Err(ActrCliError::config_error("No proto files found"));
333        }
334
335        Ok(proto_files)
336    }
337
338    /// Collect proto files recursively
339    #[allow(clippy::only_used_in_recursion)]
340    fn collect_proto_files(&self, dir: &PathBuf, proto_files: &mut Vec<PathBuf>) -> Result<()> {
341        for entry in std::fs::read_dir(dir)
342            .map_err(|e| ActrCliError::config_error(format!("Failed to read directory: {e}")))?
343        {
344            let entry = entry.map_err(|e| ActrCliError::config_error(e.to_string()))?;
345            let path = entry.path();
346
347            if path.is_file() && path.extension().unwrap_or_default() == "proto" {
348                proto_files.push(path);
349            } else if path.is_dir() {
350                self.collect_proto_files(&path, proto_files)?;
351            }
352        }
353        Ok(())
354    }
355
356    /// 确保 protoc-gen-actrframework 插件可用
357    ///
358    /// Plugin version policy:
359    /// 1. Check installed version first.
360    /// 2. If `.protoc-plugin.toml` defines a minimum version, accept any version >= minimum.
361    /// 3. Otherwise, require an exact match with the CLI version.
362    /// 4. If missing or below requirement, install/upgrade automatically.
363    fn ensure_protoc_plugin(&self) -> Result<PathBuf> {
364        // Expected version (same as actr-framework-protoc-codegen)
365        const EXPECTED_VERSION: &str = env!("CARGO_PKG_VERSION");
366        const PLUGIN_NAME: &str = "protoc-gen-actrframework";
367
368        let min_version = self.resolve_plugin_min_version(PLUGIN_NAME)?;
369        let require_exact = min_version.is_none();
370        let required_version = min_version.unwrap_or_else(|| EXPECTED_VERSION.to_string());
371
372        // 1. Check installed version
373        let installed_version = self.check_installed_plugin_version()?;
374
375        match installed_version {
376            Some(version) if self.version_satisfies(&version, &required_version, require_exact) => {
377                // Version matches, use it directly
378                info!("✅ Using installed protoc-gen-actrframework v{}", version);
379                let output = StdCommand::new("which")
380                    .arg(PLUGIN_NAME)
381                    .output()
382                    .map_err(|e| {
383                        ActrCliError::command_error(format!("Failed to locate plugin: {e}"))
384                    })?;
385
386                let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
387                Ok(PathBuf::from(path))
388            }
389            Some(version) => {
390                // Version mismatch, upgrade needed
391                if require_exact {
392                    info!(
393                        "🔄 Version mismatch: installed v{}, need v{}",
394                        version, required_version
395                    );
396                } else {
397                    info!(
398                        "🔄 Version below minimum: installed v{}, need >= v{}",
399                        version, required_version
400                    );
401                }
402                info!("🔨 Upgrading plugin...");
403                let path = self.install_or_upgrade_plugin()?;
404                self.ensure_required_plugin_version(&required_version, require_exact)?;
405                Ok(path)
406            }
407            None => {
408                // Not installed, install it
409                info!("📦 protoc-gen-actrframework not found, installing...");
410                let path = self.install_or_upgrade_plugin()?;
411                self.ensure_required_plugin_version(&required_version, require_exact)?;
412                Ok(path)
413            }
414        }
415    }
416
417    /// Check installed plugin version
418    fn check_installed_plugin_version(&self) -> Result<Option<String>> {
419        let output = StdCommand::new("protoc-gen-actrframework")
420            .arg("--version")
421            .output();
422
423        match output {
424            Ok(output) if output.status.success() => {
425                let version_info = String::from_utf8_lossy(&output.stdout);
426                // Parse "protoc-gen-actrframework 0.1.0"
427                let version = version_info
428                    .lines()
429                    .next()
430                    .and_then(|line| line.split_whitespace().nth(1))
431                    .map(|v| v.to_string());
432
433                debug!("Detected installed version: {:?}", version);
434                Ok(version)
435            }
436            _ => {
437                debug!("Plugin not found in PATH");
438                Ok(None)
439            }
440        }
441    }
442
443    /// Install or upgrade plugin from workspace
444    fn install_or_upgrade_plugin(&self) -> Result<PathBuf> {
445        // Find actr workspace
446        let current_dir = std::env::current_dir()?;
447        let workspace_root = current_dir.ancestors().find(|p| {
448            let is_workspace =
449                p.join("Cargo.toml").exists() && p.join("crates/framework-protoc-codegen").exists();
450            if is_workspace {
451                debug!("Found workspace root: {:?}", p);
452            }
453            is_workspace
454        });
455
456        let workspace_root = workspace_root.ok_or_else(|| {
457            ActrCliError::config_error(
458                "Cannot find actr workspace.\n\
459                 Please run this command from within an actr project or workspace.",
460            )
461        })?;
462
463        info!("🔍 Found actr workspace at: {}", workspace_root.display());
464
465        // Step 1: Build the plugin
466        info!("🔨 Building protoc-gen-actrframework...");
467        let mut build_cmd = StdCommand::new("cargo");
468        build_cmd
469            .arg("build")
470            .arg("-p")
471            .arg("actr-framework-protoc-codegen")
472            .arg("--bin")
473            .arg("protoc-gen-actrframework")
474            .current_dir(workspace_root);
475
476        debug!("Running: {:?}", build_cmd);
477        let output = build_cmd
478            .output()
479            .map_err(|e| ActrCliError::command_error(format!("Failed to build plugin: {e}")))?;
480
481        if !output.status.success() {
482            let stderr = String::from_utf8_lossy(&output.stderr);
483            return Err(ActrCliError::command_error(format!(
484                "Failed to build plugin:\n{stderr}"
485            )));
486        }
487
488        // Step 2: Install to ~/.cargo/bin/
489        info!("📦 Installing to ~/.cargo/bin/...");
490        let mut install_cmd = StdCommand::new("cargo");
491        install_cmd
492            .arg("install")
493            .arg("--path")
494            .arg(workspace_root.join("crates/framework-protoc-codegen"))
495            .arg("--bin")
496            .arg("protoc-gen-actrframework")
497            .arg("--force"); // Overwrite existing version
498
499        debug!("Running: {:?}", install_cmd);
500        let output = install_cmd
501            .output()
502            .map_err(|e| ActrCliError::command_error(format!("Failed to install plugin: {e}")))?;
503
504        if !output.status.success() {
505            let stderr = String::from_utf8_lossy(&output.stderr);
506            return Err(ActrCliError::command_error(format!(
507                "Failed to install plugin:\n{stderr}"
508            )));
509        }
510
511        info!("✅ Plugin installed successfully");
512
513        // Return the installed path
514        let which_output = StdCommand::new("which")
515            .arg("protoc-gen-actrframework")
516            .output()
517            .map_err(|e| {
518                ActrCliError::command_error(format!("Failed to locate installed plugin: {e}"))
519            })?;
520
521        let path = String::from_utf8_lossy(&which_output.stdout)
522            .trim()
523            .to_string();
524        Ok(PathBuf::from(path))
525    }
526
527    fn resolve_plugin_min_version(&self, plugin_name: &str) -> Result<Option<String>> {
528        let config = load_protoc_plugin_config(&self.config)?;
529        if let Some(config) = config
530            && let Some(min_version) = config.min_version(plugin_name)
531        {
532            info!(
533                "🔧 Using minimum version for {} from {}",
534                plugin_name,
535                config.path().display()
536            );
537            return Ok(Some(min_version.to_string()));
538        }
539        Ok(None)
540    }
541
542    fn version_satisfies(&self, installed: &str, required: &str, strict_equal: bool) -> bool {
543        if strict_equal {
544            installed == required
545        } else {
546            version_is_at_least(installed, required)
547        }
548    }
549
550    fn ensure_required_plugin_version(
551        &self,
552        required_version: &str,
553        strict_equal: bool,
554    ) -> Result<()> {
555        let installed_version = self.check_installed_plugin_version()?;
556        let Some(installed_version) = installed_version else {
557            return Err(ActrCliError::command_error(
558                "Failed to determine installed protoc-gen-actrframework version after install"
559                    .to_string(),
560            ));
561        };
562
563        if self.version_satisfies(&installed_version, required_version, strict_equal) {
564            return Ok(());
565        }
566
567        if strict_equal {
568            Err(ActrCliError::command_error(format!(
569                "protoc-gen-actrframework version {} does not match required version {}",
570                installed_version, required_version
571            )))
572        } else {
573            Err(ActrCliError::command_error(format!(
574                "protoc-gen-actrframework version {} is lower than minimum version {}",
575                installed_version, required_version
576            )))
577        }
578    }
579
580    /// 生成基础设施代码
581    async fn generate_infrastructure_code(
582        &self,
583        proto_files: &[PathBuf],
584        config: &actr_config::Config,
585    ) -> Result<()> {
586        info!("🔧 Generating infrastructure code...");
587
588        // 确保 protoc 插件可用
589        let plugin_path = self.ensure_protoc_plugin()?;
590
591        let manufacturer = config.package.actr_type.manufacturer.clone();
592        debug!("Using manufacturer from Actr.toml: {}", manufacturer);
593
594        // 确定输出路径
595        let output = self.determine_output_path()?;
596
597        for proto_file in proto_files {
598            debug!("Processing proto file: {:?}", proto_file);
599
600            // 第一步:使用 prost 生成基础 protobuf 消息类型
601            let mut cmd = StdCommand::new("protoc");
602            cmd.arg(format!("--proto_path={}", self.input.display()))
603                .arg("--prost_opt=flat_output_dir")
604                .arg(format!("--prost_out={}", output.display()))
605                .arg(proto_file);
606
607            debug!("Executing protoc (prost): {:?}", cmd);
608            let output_cmd = cmd.output().map_err(|e| {
609                ActrCliError::command_error(format!("Failed to execute protoc (prost): {e}"))
610            })?;
611
612            if !output_cmd.status.success() {
613                let stderr = String::from_utf8_lossy(&output_cmd.stderr);
614                return Err(ActrCliError::command_error(format!(
615                    "protoc (prost) execution failed: {stderr}"
616                )));
617            }
618
619            // 第二步:使用 actrframework 插件生成 Actor 框架代码
620            let mut cmd = StdCommand::new("protoc");
621            cmd.arg(format!("--proto_path={}", self.input.display()))
622                .arg(format!(
623                    "--plugin=protoc-gen-actrframework={}",
624                    plugin_path.display()
625                ))
626                .arg(format!("--actrframework_opt=manufacturer={manufacturer}"))
627                .arg(format!("--actrframework_out={}", output.display()))
628                .arg(proto_file);
629
630            debug!("Executing protoc (actrframework): {:?}", cmd);
631            let output_cmd = cmd.output().map_err(|e| {
632                ActrCliError::command_error(format!(
633                    "Failed to execute protoc (actrframework): {e}"
634                ))
635            })?;
636
637            if !output_cmd.status.success() {
638                let stderr = String::from_utf8_lossy(&output_cmd.stderr);
639                return Err(ActrCliError::command_error(format!(
640                    "protoc (actrframework) execution failed: {stderr}"
641                )));
642            }
643
644            let stdout = String::from_utf8_lossy(&output_cmd.stdout);
645            if !stdout.is_empty() {
646                debug!("protoc output: {}", stdout);
647            }
648        }
649
650        // 生成 mod.rs
651        self.generate_mod_rs(proto_files).await?;
652
653        info!("✅ Infrastructure code generation completed");
654        Ok(())
655    }
656
657    /// 生成 mod.rs 文件
658    async fn generate_mod_rs(&self, _proto_files: &[PathBuf]) -> Result<()> {
659        let output = self.determine_output_path()?;
660        let mod_path = output.join("mod.rs");
661
662        // 扫描实际生成的文件,而不是根据 proto 文件名猜测
663        let mut proto_modules = Vec::new();
664        let mut service_modules = Vec::new();
665
666        use std::fs;
667        for entry in fs::read_dir(&output).map_err(|e| {
668            ActrCliError::config_error(format!("Failed to read output directory: {e}"))
669        })? {
670            let entry = entry.map_err(|e| ActrCliError::config_error(e.to_string()))?;
671            let path = entry.path();
672
673            if path.is_file()
674                && path.extension().unwrap_or_default() == "rs"
675                && let Some(file_name) = path.file_stem().and_then(|s| s.to_str())
676            {
677                // 跳过 mod.rs 本身
678                if file_name == "mod" {
679                    continue;
680                }
681
682                // 区分 service_actor 文件和 proto 文件
683                if file_name.ends_with("_service_actor") {
684                    service_modules.push(format!("pub mod {file_name};"));
685                } else {
686                    proto_modules.push(format!("pub mod {file_name};"));
687                }
688            }
689        }
690
691        // 排序以保证生成的 mod.rs 内容稳定
692        proto_modules.sort();
693        service_modules.sort();
694
695        let mod_content = format!(
696            r#"//! Automatically generated code module
697//!
698//! This module is automatically generated by the `actr gen` command, including:
699//! - protobuf message type definitions
700//! - Actor framework code (router, traits)
701//!
702//! ⚠️ Do not manually modify files in this directory
703
704// Protobuf message types (generated by prost)
705{}
706
707// Actor framework code (generated by protoc-gen-actrframework)
708{}
709
710// Common types are defined in their respective modules, please import as needed
711"#,
712            proto_modules.join("\n"),
713            service_modules.join("\n"),
714        );
715
716        std::fs::write(&mod_path, mod_content)
717            .map_err(|e| ActrCliError::config_error(format!("Failed to write mod.rs: {e}")))?;
718
719        debug!("Generated mod.rs: {:?}", mod_path);
720        Ok(())
721    }
722
723    /// 将生成目录中的文件设置为只读
724    fn set_generated_files_readonly(&self) -> Result<()> {
725        use std::fs;
726
727        let output = self.determine_output_path()?;
728        for entry in fs::read_dir(&output).map_err(|e| {
729            ActrCliError::config_error(format!("Failed to read output directory: {e}"))
730        })? {
731            let entry = entry.map_err(|e| ActrCliError::config_error(e.to_string()))?;
732            let path = entry.path();
733
734            if path.is_file() && path.extension().unwrap_or_default() == "rs" {
735                // 获取当前权限
736                let metadata = fs::metadata(&path).map_err(|e| {
737                    ActrCliError::config_error(format!("Failed to get file metadata: {e}"))
738                })?;
739                let mut permissions = metadata.permissions();
740
741                // 设置只读(移除写权限)
742                #[cfg(unix)]
743                {
744                    use std::os::unix::fs::PermissionsExt;
745                    let mode = permissions.mode();
746                    permissions.set_mode(mode & !0o222); // 移除所有写权限
747                }
748
749                #[cfg(not(unix))]
750                {
751                    permissions.set_readonly(true);
752                }
753
754                fs::set_permissions(&path, permissions).map_err(|e| {
755                    ActrCliError::config_error(format!("Failed to set file permissions: {e}"))
756                })?;
757
758                debug!("Set read-only attribute: {:?}", path);
759            }
760        }
761
762        Ok(())
763    }
764
765    /// Generate user code scaffold
766    async fn generate_user_code_scaffold(&self, proto_files: &[PathBuf]) -> Result<()> {
767        info!("📝 Generating user code scaffold...");
768
769        for proto_file in proto_files {
770            let service_name = proto_file
771                .file_stem()
772                .and_then(|s| s.to_str())
773                .ok_or_else(|| ActrCliError::config_error("Invalid proto file name"))?;
774
775            self.generate_service_scaffold(service_name).await?;
776        }
777
778        info!("✅ User code scaffold generation completed");
779        Ok(())
780    }
781
782    /// Generate scaffold for a specific service
783    async fn generate_service_scaffold(&self, service_name: &str) -> Result<()> {
784        let output = self.determine_output_path()?;
785        let user_file_path = output
786            .parent()
787            .unwrap_or_else(|| Path::new("src"))
788            .join(format!("{}_service.rs", service_name.to_lowercase()));
789
790        // If file exists and overwrite is not forced, skip
791        if user_file_path.exists() && !self.overwrite_user_code {
792            info!("⏭️  Skipping existing user code file: {:?}", user_file_path);
793            return Ok(());
794        }
795
796        let scaffold_content = self.generate_scaffold_content(service_name);
797
798        std::fs::write(&user_file_path, scaffold_content).map_err(|e| {
799            ActrCliError::config_error(format!("Failed to write user code scaffold: {e}"))
800        })?;
801
802        info!("📄 Generated user code scaffold: {:?}", user_file_path);
803        Ok(())
804    }
805
806    /// 生成用户代码框架内容
807    fn generate_scaffold_content(&self, service_name: &str) -> String {
808        let service_name_pascal = service_name
809            .split('_')
810            .map(|s| {
811                let mut chars = s.chars();
812                match chars.next() {
813                    None => String::new(),
814                    Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
815                }
816            })
817            .collect::<String>();
818
819        let template = format!(
820            r#"//! # {service_name_pascal} user business logic implementation
821//!
822//! This file is a user code scaffold automatically generated by the `actr gen` command.
823//! Please implement your specific business logic here.
824
825use crate::generated::{{{service_name_pascal}Handler, {service_name_pascal}Actor}};
826// 只导入必要的类型,避免拉入不需要的依赖如 sqlite
827// use actr_framework::prelude::*;
828use std::sync::Arc;
829
830/// Specific implementation of the {service_name_pascal} service
831/// 
832/// TODO: Add state fields you need, for example:
833/// - Database connection pool
834/// - Configuration information
835/// - Cache client
836/// - Logger, etc.
837pub struct My{service_name_pascal}Service {{
838    // TODO: Add your service state fields
839    // For example:
840    // pub db_pool: Arc<DatabasePool>,
841    // pub config: Arc<ServiceConfig>,
842    // pub metrics: Arc<Metrics>,
843}}
844
845impl My{service_name_pascal}Service {{
846    /// Create a new service instance
847    /// 
848    /// TODO: Modify constructor parameters as needed
849    pub fn new(/* TODO: Add necessary dependencies */) -> Self {{
850        Self {{
851            // TODO: Initialize your fields
852        }}
853    }}
854    
855    /// Create a service instance with default configuration (for testing)
856    pub fn default_for_testing() -> Self {{
857        Self {{
858            // TODO: Provide default values for testing
859        }}
860    }}
861}}
862
863// TODO: Implement all methods of the {service_name_pascal}Handler trait
864// Note: The impl_user_code_scaffold! macro has generated a basic scaffold for you,
865// you need to replace it with real business logic implementation.
866//
867// Example:
868// #[async_trait]
869// impl {service_name_pascal}Handler for My{service_name_pascal}Service {{
870//     async fn method_name(&self, req: RequestType) -> ActorResult<ResponseType> {{
871//         // 1. Validate input
872//         // 2. Execute business logic
873//         // 3. Return result
874//         todo!("Implement your business logic")
875//     }}
876// }}
877
878#[cfg(test)]
879mod tests {{
880    use super::*;
881    
882    #[tokio::test]
883    async fn test_service_creation() {{
884        let _service = My{service_name_pascal}Service::default_for_testing();
885        // TODO: Add your tests
886    }}
887    
888    // TODO: Add more test cases
889}}
890
891/*
892📚 User Guide
893
894## 🚀 Quick Start
895
8961. **Implement business logic**:
897   Implement all methods of the `{service_name_pascal}Handler` trait in `My{service_name_pascal}Service`
898
8992. **Add dependencies**:
900   Add dependencies you need in `Cargo.toml`, such as database clients, HTTP clients, etc.
901
9023. **Configure service**:
903   Modify the `new()` constructor to inject necessary dependencies
904
9054. **Start service**:
906   ```rust
907   #[tokio::main]
908   async fn main() -> ActorResult<()> {{
909       let service = My{service_name_pascal}Service::new(/* dependencies */);
910       
911       ActorSystem::new()
912           .attach(service)
913           .start()
914           .await
915   }}
916   ```
917
918## 🔧 Development Tips
919
920- Use `tracing` crate for logging
921- Implement error handling and retry logic
922- Add unit and integration tests
923- Consider using configuration files for environment variables
924- Implement health checks and metrics collection
925
926## 📖 More Resources
927
928- Actor-RTC Documentation: [Link]
929- API Reference: [Link]
930- Example Projects: [Link]
931*/
932"# // Service in example code
933        );
934
935        template
936    }
937
938    /// 格式化生成的代码
939    async fn format_generated_code(&self) -> Result<()> {
940        info!("🎨 Formatting generated code...");
941
942        let mut cmd = StdCommand::new("rustfmt");
943        cmd.arg("--edition")
944            .arg("2024")
945            .arg("--config")
946            .arg("max_width=100");
947
948        // 格式化生成目录中的所有 .rs 文件
949        let output = self.determine_output_path()?;
950        for entry in std::fs::read_dir(&output).map_err(|e| {
951            ActrCliError::config_error(format!("Failed to read output directory: {e}"))
952        })? {
953            let entry = entry.map_err(|e| ActrCliError::config_error(e.to_string()))?;
954            let path = entry.path();
955
956            if path.extension().unwrap_or_default() == "rs" {
957                cmd.arg(&path);
958            }
959        }
960
961        let output = cmd
962            .output()
963            .map_err(|e| ActrCliError::command_error(format!("Failed to execute rustfmt: {e}")))?;
964
965        if !output.status.success() {
966            let stderr = String::from_utf8_lossy(&output.stderr);
967            warn!("rustfmt execution warning: {}", stderr);
968        } else {
969            info!("✅ Code formatting completed");
970        }
971
972        Ok(())
973    }
974
975    /// 验证生成的代码
976    async fn validate_generated_code(&self) -> Result<()> {
977        info!("🔍 Validating generated code...");
978
979        // 查找项目根目录(包含 Cargo.toml 的目录)
980        let project_root = self.find_project_root()?;
981
982        let mut cmd = StdCommand::new("cargo");
983        cmd.arg("check").arg("--quiet").current_dir(&project_root);
984
985        let output = cmd.output().map_err(|e| {
986            ActrCliError::command_error(format!("Failed to execute cargo check: {e}"))
987        })?;
988
989        if !output.status.success() {
990            let stderr = String::from_utf8_lossy(&output.stderr);
991            warn!(
992                "Generated code has compilation warnings or errors:\n{}",
993                stderr
994            );
995            info!("💡 This is usually normal because the user code scaffold contains TODO markers");
996        } else {
997            info!("✅ Code validation passed");
998        }
999
1000        Ok(())
1001    }
1002
1003    /// 查找项目根目录(包含 Cargo.toml 的目录)
1004    fn find_project_root(&self) -> Result<PathBuf> {
1005        let mut current = std::env::current_dir().map_err(ActrCliError::Io)?;
1006
1007        loop {
1008            if current.join("Cargo.toml").exists() {
1009                return Ok(current);
1010            }
1011
1012            match current.parent() {
1013                Some(parent) => current = parent.to_path_buf(),
1014                None => break,
1015            }
1016        }
1017
1018        // 如果找不到 Cargo.toml,回退到当前目录
1019        std::env::current_dir().map_err(ActrCliError::Io)
1020    }
1021
1022    /// 打印后续步骤提示
1023    fn print_next_steps(&self) {
1024        println!("\n🎉 Code generation completed!");
1025        println!("\n📋 Next steps:");
1026        let output = self
1027            .determine_output_path()
1028            .unwrap_or_else(|_| PathBuf::from("src/generated"));
1029        println!("1. 📖 View generated code: {:?}", output);
1030        if self.should_generate_scaffold() {
1031            println!(
1032                "2. ✏️  Implement business logic: in the *_service.rs files in the src/ directory"
1033            );
1034            println!("3. 🔧 Add dependencies: add required packages in Cargo.toml");
1035            println!("4. 🏗️  Build project: cargo build");
1036            println!("5. 🧪 Run tests: cargo test");
1037            println!("6. 🚀 Start service: cargo run");
1038        } else {
1039            println!("2. 🏗️  Build project: cargo build");
1040            println!("3. 🧪 Run tests: cargo test");
1041            println!("4. 🚀 Start service: cargo run");
1042        }
1043        println!("\n💡 Tip: Check the detailed user guide in the generated user code files");
1044    }
1045}