syncable_cli/agent/tools/platform/
analyze_codebase.rs1use 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#[derive(Debug, Deserialize)]
21pub struct AnalyzeCodebaseArgs {
22 #[serde(default = "default_project_path")]
24 pub project_path: String,
25 #[serde(default)]
27 pub include_dev_dependencies: bool,
28}
29
30fn default_project_path() -> String {
31 ".".to_string()
32}
33
34#[derive(Debug, thiserror::Error)]
36#[error("Analyze codebase error: {0}")]
37pub struct AnalyzeCodebaseError(String);
38
39#[derive(Debug, Clone, Serialize, Deserialize, Default)]
44pub struct AnalyzeCodebaseTool;
45
46impl AnalyzeCodebaseTool {
47 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 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 let config = AnalysisConfig {
144 include_dev_dependencies: args.include_dev_dependencies,
145 deep_analysis: true,
146 ..Default::default()
147 };
148
149 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
170fn format_analysis_for_llm(project_path: &str, analysis: &ProjectAnalysis) -> serde_json::Value {
172 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 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 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 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 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 let deployment_hints = derive_deployment_hints(analysis);
244
245 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
269fn 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
284fn derive_deployment_hints(analysis: &ProjectAnalysis) -> serde_json::Value {
286 let suggested_port = analysis
288 .ports
289 .first()
290 .map(|p| p.number)
291 .or_else(|| infer_default_port(analysis));
292
293 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 let recommended_dockerfile_base = infer_dockerfile_base(analysis);
304
305 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
324fn 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 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
369fn infer_dockerfile_base(analysis: &ProjectAnalysis) -> Option<String> {
371 for lang in &analysis.languages {
373 match lang.name.to_lowercase().as_str() {
374 "javascript" | "typescript" => {
375 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
393fn 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
426fn 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}