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