syncable_cli/agent/tools/platform/
analyze_project.rs

1//! Analyze project tool for the agent
2//!
3//! Wraps the existing `discover_dockerfiles_for_deployment` analyzer function
4//! to allow the agent to analyze projects for deployment.
5
6use rig::completion::ToolDefinition;
7use rig::tool::Tool;
8use serde::{Deserialize, Serialize};
9use serde_json::json;
10use std::path::Path;
11
12use crate::agent::tools::error::{ErrorCategory, format_error_for_llm};
13use crate::analyzer::discover_dockerfiles_for_deployment;
14
15/// Arguments for the analyze project tool
16#[derive(Debug, Deserialize)]
17pub struct AnalyzeProjectArgs {
18    /// Path to the project directory to analyze (defaults to current directory)
19    #[serde(default = "default_project_path")]
20    pub project_path: String,
21}
22
23fn default_project_path() -> String {
24    ".".to_string()
25}
26
27/// Error type for analyze project operations
28#[derive(Debug, thiserror::Error)]
29#[error("Analyze project error: {0}")]
30pub struct AnalyzeProjectError(String);
31
32/// Tool to analyze a project directory for deployment
33///
34/// Discovers Dockerfiles and their build configurations to help
35/// prepare for deployment.
36#[derive(Debug, Clone, Serialize, Deserialize, Default)]
37pub struct AnalyzeProjectTool;
38
39impl AnalyzeProjectTool {
40    /// Create a new AnalyzeProjectTool
41    pub fn new() -> Self {
42        Self
43    }
44}
45
46impl Tool for AnalyzeProjectTool {
47    const NAME: &'static str = "analyze_project";
48
49    type Error = AnalyzeProjectError;
50    type Args = AnalyzeProjectArgs;
51    type Output = String;
52
53    async fn definition(&self, _prompt: String) -> ToolDefinition {
54        ToolDefinition {
55            name: Self::NAME.to_string(),
56            description: r#"Analyze a project directory to discover Dockerfiles and build configurations for deployment.
57
58Before deploying, use this tool to understand what can be deployed from a project.
59
60**What it detects:**
61- Dockerfiles and their variants (Dockerfile.dev, Dockerfile.prod, etc.)
62- Build context paths for each Dockerfile
63- Exposed ports from EXPOSE instructions or inferred from base images
64- Multi-stage build configurations
65- Suggested service names based on directory structure
66
67**Parameters:**
68- project_path: Path to the project directory (defaults to ".")
69
70**Use Cases:**
71- Before creating a deployment config, analyze the project structure
72- Understand what services can be deployed from a monorepo
73- Find the correct Dockerfile and build context for deployment
74
75**Returns:**
76- dockerfiles: Array of discovered Dockerfiles with deployment metadata
77- summary: Human-readable summary of what was found"#
78                .to_string(),
79            parameters: json!({
80                "type": "object",
81                "properties": {
82                    "project_path": {
83                        "type": "string",
84                        "description": "Path to the project directory to analyze (defaults to current directory)",
85                        "default": "."
86                    }
87                },
88                "required": []
89            }),
90        }
91    }
92
93    async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
94        let project_path = Path::new(&args.project_path);
95
96        // Validate path exists
97        if !project_path.exists() {
98            return Ok(format_error_for_llm(
99                "analyze_project",
100                ErrorCategory::FileNotFound,
101                &format!("Project path does not exist: {}", args.project_path),
102                Some(vec![
103                    "Check that the path is correct",
104                    "Use an absolute path or path relative to current directory",
105                ]),
106            ));
107        }
108
109        if !project_path.is_dir() {
110            return Ok(format_error_for_llm(
111                "analyze_project",
112                ErrorCategory::ValidationFailed,
113                &format!("Path is not a directory: {}", args.project_path),
114                Some(vec!["Provide a directory path, not a file path"]),
115            ));
116        }
117
118        // Call the existing analyzer function
119        match discover_dockerfiles_for_deployment(project_path) {
120            Ok(dockerfiles) => {
121                let dockerfile_count = dockerfiles.len();
122
123                // Build response with discovered Dockerfiles
124                let dockerfile_data: Vec<serde_json::Value> = dockerfiles
125                    .into_iter()
126                    .map(|df| {
127                        json!({
128                            "path": df.path.display().to_string(),
129                            "build_context": df.build_context,
130                            "suggested_service_name": df.suggested_service_name,
131                            "suggested_port": df.suggested_port,
132                            "base_image": df.base_image,
133                            "is_multistage": df.is_multistage,
134                            "environment": df.environment,
135                        })
136                    })
137                    .collect();
138
139                let summary = if dockerfile_count == 0 {
140                    "No Dockerfiles found in this project. You may need to create a Dockerfile before deploying.".to_string()
141                } else {
142                    format!(
143                        "Found {} Dockerfile{} suitable for deployment",
144                        dockerfile_count,
145                        if dockerfile_count == 1 { "" } else { "s" }
146                    )
147                };
148
149                let result = json!({
150                    "success": true,
151                    "project_path": args.project_path,
152                    "dockerfiles": dockerfile_data,
153                    "dockerfile_count": dockerfile_count,
154                    "summary": summary,
155                    "next_steps": if dockerfile_count > 0 {
156                        vec![
157                            "Use analyze_codebase for deeper analysis of build requirements and environment variables",
158                            "Use list_deployment_capabilities to see available deployment targets",
159                            "Use create_deployment_config to create a deployment configuration"
160                        ]
161                    } else {
162                        vec![
163                            "Use analyze_codebase to understand the project's technology stack and recommended Dockerfile base image",
164                            "Create a Dockerfile for your application",
165                            "Consider using a multi-stage build for smaller images"
166                        ]
167                    }
168                });
169
170                serde_json::to_string_pretty(&result)
171                    .map_err(|e| AnalyzeProjectError(format!("Failed to serialize: {}", e)))
172            }
173            Err(e) => Ok(format_error_for_llm(
174                "analyze_project",
175                ErrorCategory::InternalError,
176                &format!("Failed to analyze project: {}", e),
177                Some(vec![
178                    "Check that you have read permissions for the project directory",
179                    "Ensure the path is accessible",
180                ]),
181            )),
182        }
183    }
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn test_tool_name() {
192        assert_eq!(AnalyzeProjectTool::NAME, "analyze_project");
193    }
194
195    #[test]
196    fn test_tool_creation() {
197        let tool = AnalyzeProjectTool::new();
198        assert!(format!("{:?}", tool).contains("AnalyzeProjectTool"));
199    }
200
201    #[test]
202    fn test_default_project_path() {
203        assert_eq!(default_project_path(), ".");
204    }
205}