syncable_cli/agent/tools/
generate.rs

1//! IaC Generation tool for the agent
2//!
3//! Wraps the existing generator functionality for the agent to use.
4
5use rig::completion::ToolDefinition;
6use rig::tool::Tool;
7use serde::{Deserialize, Serialize};
8use serde_json::json;
9use std::path::PathBuf;
10
11use crate::analyzer::analyze_monorepo;
12use crate::generator;
13
14/// Arguments for the generate IaC tool
15#[derive(Debug, Deserialize)]
16pub struct GenerateIaCArgs {
17    /// Type of IaC to generate: "dockerfile", "compose", "terraform", or "all"
18    pub generate_type: String,
19    /// Optional subdirectory to generate for
20    pub path: Option<String>,
21}
22
23/// Error type for generate tool
24#[derive(Debug, thiserror::Error)]
25#[error("Generation error: {0}")]
26pub struct GenerateIaCError(String);
27
28/// Tool to generate Infrastructure as Code
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct GenerateIaCTool {
31    project_path: PathBuf,
32}
33
34impl GenerateIaCTool {
35    pub fn new(project_path: PathBuf) -> Self {
36        Self { project_path }
37    }
38}
39
40impl Tool for GenerateIaCTool {
41    const NAME: &'static str = "generate_iac";
42
43    type Error = GenerateIaCError;
44    type Args = GenerateIaCArgs;
45    type Output = String;
46
47    async fn definition(&self, _prompt: String) -> ToolDefinition {
48        ToolDefinition {
49            name: Self::NAME.to_string(),
50            description: "Generate Infrastructure as Code files based on project analysis. Can generate Dockerfiles, Docker Compose configurations, or Terraform files. Returns the generated content as a preview without writing to disk.".to_string(),
51            parameters: json!({
52                "type": "object",
53                "properties": {
54                    "generate_type": {
55                        "type": "string",
56                        "enum": ["dockerfile", "compose", "terraform", "all"],
57                        "description": "Type of IaC to generate: 'dockerfile' for container config, 'compose' for Docker Compose, 'terraform' for infrastructure, 'all' for everything"
58                    },
59                    "path": {
60                        "type": "string",
61                        "description": "Optional subdirectory to analyze for generation (relative to project root)"
62                    }
63                },
64                "required": ["generate_type"]
65            }),
66        }
67    }
68
69    async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
70        let path = if let Some(subpath) = args.path {
71            self.project_path.join(subpath)
72        } else {
73            self.project_path.clone()
74        };
75
76        // Run analysis
77        let monorepo_analysis = analyze_monorepo(&path)
78            .map_err(|e| GenerateIaCError(format!("Analysis failed: {}", e)))?;
79
80        // Get the main project analysis
81        let main_project = &monorepo_analysis.projects[0];
82        let analysis = &main_project.analysis;
83
84        let generate_type = args.generate_type.to_lowercase();
85        let generate_all = generate_type == "all";
86
87        let mut results = Vec::new();
88
89        // Generate Dockerfile
90        if generate_all || generate_type == "dockerfile" {
91            match generator::generate_dockerfile(analysis) {
92                Ok(content) => {
93                    results.push(json!({
94                        "type": "Dockerfile",
95                        "content": content,
96                        "filename": "Dockerfile"
97                    }));
98                }
99                Err(e) => {
100                    results.push(json!({
101                        "type": "Dockerfile",
102                        "error": e.to_string()
103                    }));
104                }
105            }
106        }
107
108        // Generate Docker Compose
109        if generate_all || generate_type == "compose" {
110            match generator::generate_compose(analysis) {
111                Ok(content) => {
112                    results.push(json!({
113                        "type": "Docker Compose",
114                        "content": content,
115                        "filename": "docker-compose.yml"
116                    }));
117                }
118                Err(e) => {
119                    results.push(json!({
120                        "type": "Docker Compose",
121                        "error": e.to_string()
122                    }));
123                }
124            }
125        }
126
127        // Generate Terraform
128        if generate_all || generate_type == "terraform" {
129            match generator::generate_terraform(analysis) {
130                Ok(content) => {
131                    results.push(json!({
132                        "type": "Terraform",
133                        "content": content,
134                        "filename": "main.tf"
135                    }));
136                }
137                Err(e) => {
138                    results.push(json!({
139                        "type": "Terraform",
140                        "error": e.to_string()
141                    }));
142                }
143            }
144        }
145
146        // Add project context to help the agent
147        let project_info = json!({
148            "project_name": main_project.name,
149            "languages": monorepo_analysis.technology_summary.languages,
150            "frameworks": monorepo_analysis.technology_summary.frameworks,
151            "is_monorepo": monorepo_analysis.is_monorepo,
152            "project_count": monorepo_analysis.projects.len()
153        });
154
155        let result = json!({
156            "generated": results,
157            "project_info": project_info,
158            "note": "This is a preview. The content has not been written to disk. Share with the user and ask if they want to save these files."
159        });
160
161        serde_json::to_string_pretty(&result)
162            .map_err(|e| GenerateIaCError(format!("Serialization error: {}", e)))
163    }
164}