Skip to main content

syncable_cli/agent/tools/platform/
analyze_codebase.rs

1//! Analyze codebase tool for the agent
2//!
3//! Wraps the full `analyze_project()` analyzer function to provide comprehensive
4//! project analysis including languages, frameworks, entry points, ports,
5//! environment variables, and build scripts.
6
7use rig::completion::ToolDefinition;
8use rig::tool::Tool;
9use serde::{Deserialize, Serialize};
10use serde_json::json;
11use std::path::Path;
12
13use crate::agent::tools::error::{ErrorCategory, format_error_for_llm};
14use crate::analyzer::{
15    AnalysisConfig, ProjectAnalysis, ProjectType, TechnologyCategory, analyze_project_with_config,
16};
17
18/// Arguments for the analyze codebase tool
19#[derive(Debug, Deserialize)]
20pub struct AnalyzeCodebaseArgs {
21    /// Path to the project directory to analyze (defaults to current directory)
22    #[serde(default = "default_project_path")]
23    pub project_path: String,
24    /// Whether to include dev dependencies in analysis (defaults to false)
25    #[serde(default)]
26    pub include_dev_dependencies: bool,
27}
28
29fn default_project_path() -> String {
30    ".".to_string()
31}
32
33/// Error type for analyze codebase operations
34#[derive(Debug, thiserror::Error)]
35#[error("Analyze codebase error: {0}")]
36pub struct AnalyzeCodebaseError(String);
37
38/// Tool to perform comprehensive codebase analysis
39///
40/// Provides detailed information about a project's technology stack,
41/// build requirements, and deployment configuration recommendations.
42#[derive(Debug, Clone, Serialize, Deserialize, Default)]
43pub struct AnalyzeCodebaseTool;
44
45impl AnalyzeCodebaseTool {
46    /// Create a new AnalyzeCodebaseTool
47    pub fn new() -> Self {
48        Self
49    }
50}
51
52impl Tool for AnalyzeCodebaseTool {
53    const NAME: &'static str = "analyze_codebase";
54
55    type Error = AnalyzeCodebaseError;
56    type Args = AnalyzeCodebaseArgs;
57    type Output = String;
58
59    async fn definition(&self, _prompt: String) -> ToolDefinition {
60        ToolDefinition {
61            name: Self::NAME.to_string(),
62            description: r#"Perform comprehensive analysis of a codebase to understand its technology stack and deployment requirements.
63
64**Use this tool to understand HOW to configure a deployment.** For quick Dockerfile discovery, use `analyze_project` instead.
65
66**What it detects:**
67- Programming languages with versions and confidence scores
68- Frameworks and libraries (React, Next.js, Express, Django, etc.)
69- Entry points and exposed ports
70- Environment variables the application needs
71- Build scripts (npm run build, etc.)
72- Docker configuration if present
73
74**Parameters:**
75- project_path: Path to the project directory (defaults to ".")
76- include_dev_dependencies: Include dev dependencies in analysis (default: false)
77
78**Use Cases:**
79- Understanding a project's technology stack before configuring deployment
80- Discovering required environment variables for secrets setup
81- Finding available build scripts for CI/CD configuration
82- Recommending appropriate Dockerfile base images
83
84**Returns:**
85- languages: Detected languages with versions
86- technologies: Frameworks, libraries, and tools
87- ports: Exposed ports from various sources
88- environment_variables: Environment variables the app needs
89- build_scripts: Available build commands
90- deployment_hints: Derived recommendations for deployment
91- next_steps: Guidance on what to do next
92
93**Comparison with analyze_project:**
94- `analyze_project`: Fast, focused on Dockerfiles only - "what can I deploy?"
95- `analyze_codebase`: Comprehensive analysis - "how should I configure deployment?""#
96                .to_string(),
97            parameters: json!({
98                "type": "object",
99                "properties": {
100                    "project_path": {
101                        "type": "string",
102                        "description": "Path to the project directory to analyze (defaults to current directory)",
103                        "default": "."
104                    },
105                    "include_dev_dependencies": {
106                        "type": "boolean",
107                        "description": "Include dev dependencies in analysis (default: false)",
108                        "default": false
109                    }
110                },
111                "required": []
112            }),
113        }
114    }
115
116    async fn call(&self, args: Self::Args) -> Result<Self::Output, Self::Error> {
117        let project_path = Path::new(&args.project_path);
118
119        // Validate path exists
120        if !project_path.exists() {
121            return Ok(format_error_for_llm(
122                "analyze_codebase",
123                ErrorCategory::FileNotFound,
124                &format!("Project path does not exist: {}", args.project_path),
125                Some(vec![
126                    "Check that the path is correct",
127                    "Use an absolute path or path relative to current directory",
128                ]),
129            ));
130        }
131
132        if !project_path.is_dir() {
133            return Ok(format_error_for_llm(
134                "analyze_codebase",
135                ErrorCategory::ValidationFailed,
136                &format!("Path is not a directory: {}", args.project_path),
137                Some(vec!["Provide a directory path, not a file path"]),
138            ));
139        }
140
141        // Configure analysis
142        let config = AnalysisConfig {
143            include_dev_dependencies: args.include_dev_dependencies,
144            deep_analysis: true,
145            ..Default::default()
146        };
147
148        // Perform analysis
149        match analyze_project_with_config(project_path, &config) {
150            Ok(analysis) => {
151                let result = format_analysis_for_llm(&args.project_path, &analysis);
152                serde_json::to_string_pretty(&result)
153                    .map_err(|e| AnalyzeCodebaseError(format!("Failed to serialize: {}", e)))
154            }
155            Err(e) => Ok(format_error_for_llm(
156                "analyze_codebase",
157                ErrorCategory::InternalError,
158                &format!("Failed to analyze codebase: {}", e),
159                Some(vec![
160                    "Check that you have read permissions for the project directory",
161                    "Ensure the path is accessible",
162                    "Try running from the project root directory",
163                ]),
164            )),
165        }
166    }
167}
168
169/// Format ProjectAnalysis into LLM-friendly JSON
170fn format_analysis_for_llm(project_path: &str, analysis: &ProjectAnalysis) -> serde_json::Value {
171    // Format languages
172    let languages: Vec<serde_json::Value> = analysis
173        .languages
174        .iter()
175        .map(|lang| {
176            json!({
177                "name": lang.name,
178                "version": lang.version,
179                "confidence": lang.confidence,
180                "package_manager": lang.package_manager,
181            })
182        })
183        .collect();
184
185    // Format technologies (frameworks, libraries)
186    let technologies: Vec<serde_json::Value> = analysis
187        .technologies
188        .iter()
189        .map(|tech| {
190            json!({
191                "name": tech.name,
192                "version": tech.version,
193                "category": format_category(&tech.category),
194                "is_primary": tech.is_primary,
195                "confidence": tech.confidence,
196            })
197        })
198        .collect();
199
200    // Format ports
201    let ports: Vec<serde_json::Value> = analysis
202        .ports
203        .iter()
204        .map(|port| {
205            json!({
206                "number": port.number,
207                "protocol": format!("{:?}", port.protocol),
208                "description": port.description,
209            })
210        })
211        .collect();
212
213    // Format environment variables
214    let env_vars: Vec<serde_json::Value> = analysis
215        .environment_variables
216        .iter()
217        .map(|env| {
218            json!({
219                "name": env.name,
220                "required": env.required,
221                "default_value": env.default_value,
222                "description": env.description,
223            })
224        })
225        .collect();
226
227    // Format build scripts
228    let build_scripts: Vec<serde_json::Value> = analysis
229        .build_scripts
230        .iter()
231        .map(|script| {
232            json!({
233                "name": script.name,
234                "command": script.command,
235                "description": script.description,
236                "is_default": script.is_default,
237            })
238        })
239        .collect();
240
241    // Derive deployment hints
242    let deployment_hints = derive_deployment_hints(analysis);
243
244    // Determine next steps
245    let next_steps = determine_next_steps(analysis);
246
247    json!({
248        "success": true,
249        "project_path": project_path,
250        "languages": languages,
251        "technologies": technologies,
252        "ports": ports,
253        "environment_variables": env_vars,
254        "build_scripts": build_scripts,
255        "project_type": format!("{:?}", analysis.project_type),
256        "architecture_type": format!("{:?}", analysis.architecture_type),
257        "analysis_metadata": {
258            "confidence_score": analysis.analysis_metadata.confidence_score,
259            "files_analyzed": analysis.analysis_metadata.files_analyzed,
260            "duration_ms": analysis.analysis_metadata.analysis_duration_ms,
261        },
262        "deployment_hints": deployment_hints,
263        "summary": format_summary(analysis),
264        "next_steps": next_steps,
265    })
266}
267
268/// Format technology category for output
269fn format_category(category: &TechnologyCategory) -> String {
270    match category {
271        TechnologyCategory::MetaFramework => "MetaFramework".to_string(),
272        TechnologyCategory::FrontendFramework => "FrontendFramework".to_string(),
273        TechnologyCategory::BackendFramework => "BackendFramework".to_string(),
274        TechnologyCategory::Library(lib_type) => format!("Library:{:?}", lib_type),
275        TechnologyCategory::BuildTool => "BuildTool".to_string(),
276        TechnologyCategory::Database => "Database".to_string(),
277        TechnologyCategory::Testing => "Testing".to_string(),
278        TechnologyCategory::Runtime => "Runtime".to_string(),
279        TechnologyCategory::PackageManager => "PackageManager".to_string(),
280    }
281}
282
283/// Derive deployment hints from analysis
284fn derive_deployment_hints(analysis: &ProjectAnalysis) -> serde_json::Value {
285    // Suggested port: first detected port or framework default
286    let suggested_port = analysis
287        .ports
288        .first()
289        .map(|p| p.number)
290        .or_else(|| infer_default_port(analysis));
291
292    // Check if build step is needed
293    let needs_build_step = !analysis.build_scripts.is_empty()
294        || analysis.technologies.iter().any(|t| {
295            matches!(
296                t.category,
297                TechnologyCategory::MetaFramework | TechnologyCategory::FrontendFramework
298            )
299        });
300
301    // Recommend Dockerfile base image
302    let recommended_dockerfile_base = infer_dockerfile_base(analysis);
303
304    // Check for Docker presence
305    let has_dockerfile = analysis
306        .docker_analysis
307        .as_ref()
308        .map(|d| !d.dockerfiles.is_empty())
309        .unwrap_or(false);
310
311    json!({
312        "suggested_port": suggested_port,
313        "needs_build_step": needs_build_step,
314        "recommended_dockerfile_base": recommended_dockerfile_base,
315        "has_existing_dockerfile": has_dockerfile,
316        "required_env_vars": analysis.environment_variables.iter()
317            .filter(|e| e.required)
318            .map(|e| e.name.clone())
319            .collect::<Vec<_>>(),
320    })
321}
322
323/// Infer default port based on detected frameworks
324fn infer_default_port(analysis: &ProjectAnalysis) -> Option<u16> {
325    for tech in &analysis.technologies {
326        let name_lower = tech.name.to_lowercase();
327        if name_lower.contains("next") || name_lower.contains("nuxt") {
328            return Some(3000);
329        }
330        if name_lower.contains("vite") || name_lower.contains("vue") {
331            return Some(5173);
332        }
333        if name_lower.contains("angular") {
334            return Some(4200);
335        }
336        if name_lower.contains("django") {
337            return Some(8000);
338        }
339        if name_lower.contains("flask") {
340            return Some(5000);
341        }
342        if name_lower.contains("express") || name_lower.contains("fastify") {
343            return Some(3000);
344        }
345        if name_lower.contains("spring") {
346            return Some(8080);
347        }
348        if name_lower.contains("actix") || name_lower.contains("axum") {
349            return Some(8080);
350        }
351    }
352
353    // Default based on language
354    for lang in &analysis.languages {
355        match lang.name.to_lowercase().as_str() {
356            "python" => return Some(8000),
357            "go" => return Some(8080),
358            "rust" => return Some(8080),
359            "java" | "kotlin" => return Some(8080),
360            "javascript" | "typescript" => return Some(3000),
361            _ => {}
362        }
363    }
364
365    None
366}
367
368/// Infer recommended Dockerfile base image
369fn infer_dockerfile_base(analysis: &ProjectAnalysis) -> Option<String> {
370    // Check primary language
371    for lang in &analysis.languages {
372        match lang.name.to_lowercase().as_str() {
373            "javascript" | "typescript" => {
374                // Check for Bun
375                if analysis
376                    .technologies
377                    .iter()
378                    .any(|t| t.name.to_lowercase() == "bun")
379                {
380                    return Some("oven/bun:1-alpine".to_string());
381                }
382                return Some("node:20-alpine".to_string());
383            }
384            "python" => return Some("python:3.12-slim".to_string()),
385            "go" => return Some("golang:1.22-alpine".to_string()),
386            "rust" => return Some("rust:1.75-alpine".to_string()),
387            "java" => return Some("eclipse-temurin:21-jre-alpine".to_string()),
388            "kotlin" => return Some("eclipse-temurin:21-jre-alpine".to_string()),
389            _ => {}
390        }
391    }
392
393    None
394}
395
396/// Determine next steps based on analysis
397fn determine_next_steps(analysis: &ProjectAnalysis) -> Vec<String> {
398    let mut steps = Vec::new();
399
400    let has_dockerfile = analysis
401        .docker_analysis
402        .as_ref()
403        .map(|d| !d.dockerfiles.is_empty())
404        .unwrap_or(false);
405
406    if has_dockerfile {
407        steps.push("Use analyze_project to get specific Dockerfile details".to_string());
408        steps.push(
409            "Use list_deployment_capabilities to see available deployment targets".to_string(),
410        );
411        steps.push("Use create_deployment_config to create a deployment configuration".to_string());
412    } else {
413        steps.push(
414            "Create a Dockerfile for your application (recommended base image in deployment_hints)"
415                .to_string(),
416        );
417        steps.push(
418            "After creating Dockerfile, use analyze_project to verify it's detected".to_string(),
419        );
420    }
421
422    if !analysis.environment_variables.is_empty() {
423        let required_count = analysis
424            .environment_variables
425            .iter()
426            .filter(|e| e.required)
427            .count();
428        if required_count > 0 {
429            steps.push(format!(
430                "Configure {} required environment variable{} before deployment",
431                required_count,
432                if required_count == 1 { "" } else { "s" }
433            ));
434        }
435    }
436
437    steps
438}
439
440/// Format a human-readable summary
441fn format_summary(analysis: &ProjectAnalysis) -> String {
442    let lang_names: Vec<&str> = analysis.languages.iter().map(|l| l.name.as_str()).collect();
443
444    let primary_tech: Vec<&str> = analysis
445        .technologies
446        .iter()
447        .filter(|t| t.is_primary)
448        .map(|t| t.name.as_str())
449        .collect();
450
451    let project_type = match analysis.project_type {
452        ProjectType::WebApplication => "web application",
453        ProjectType::ApiService => "API service",
454        ProjectType::CliTool => "CLI tool",
455        ProjectType::Library => "library",
456        ProjectType::MobileApp => "mobile app",
457        ProjectType::DesktopApp => "desktop app",
458        ProjectType::Microservice => "microservice",
459        ProjectType::StaticSite => "static site",
460        ProjectType::Hybrid => "hybrid project",
461        ProjectType::Unknown => "project",
462    };
463
464    let lang_str = if lang_names.is_empty() {
465        "Unknown language".to_string()
466    } else {
467        lang_names.join(", ")
468    };
469
470    let tech_str = if primary_tech.is_empty() {
471        String::new()
472    } else {
473        format!(" using {}", primary_tech.join(", "))
474    };
475
476    format!("{} {}{}", lang_str, project_type, tech_str)
477}
478
479#[cfg(test)]
480mod tests {
481    use super::*;
482
483    #[test]
484    fn test_tool_name() {
485        assert_eq!(AnalyzeCodebaseTool::NAME, "analyze_codebase");
486    }
487
488    #[test]
489    fn test_tool_creation() {
490        let tool = AnalyzeCodebaseTool::new();
491        assert!(format!("{:?}", tool).contains("AnalyzeCodebaseTool"));
492    }
493
494    #[test]
495    fn test_default_project_path() {
496        assert_eq!(default_project_path(), ".");
497    }
498
499    #[test]
500    fn test_format_category() {
501        assert_eq!(
502            format_category(&TechnologyCategory::MetaFramework),
503            "MetaFramework"
504        );
505        assert_eq!(
506            format_category(&TechnologyCategory::BackendFramework),
507            "BackendFramework"
508        );
509    }
510}