syncable_cli/analyzer/
project_context.rs

1#[allow(unused_imports)]
2use crate::analyzer::{AnalysisConfig, DetectedTechnology, DetectedLanguage, EntryPoint, EnvVar, Port, Protocol, ProjectType, BuildScript, TechnologyCategory, LibraryType};
3use crate::error::{Result, AnalysisError};
4use crate::common::file_utils::{read_file_safe, is_readable_file};
5use std::path::{Path, PathBuf};
6use std::collections::{HashSet, HashMap};
7use regex::Regex;
8use serde_json::Value;
9
10/// Project context information
11pub struct ProjectContext {
12    pub entry_points: Vec<EntryPoint>,
13    pub ports: Vec<Port>,
14    pub environment_variables: Vec<EnvVar>,
15    pub project_type: ProjectType,
16    pub build_scripts: Vec<BuildScript>,
17}
18
19/// Helper function to create a regex with proper error handling
20fn create_regex(pattern: &str) -> Result<Regex> {
21    Regex::new(pattern)
22        .map_err(|e| AnalysisError::InvalidStructure(format!("Invalid regex pattern '{}': {}", pattern, e)).into())
23}
24
25/// Analyzes project context including entry points, ports, and environment variables
26pub fn analyze_context(
27    project_root: &Path,
28    languages: &[DetectedLanguage],
29    technologies: &[DetectedTechnology],
30    config: &AnalysisConfig,
31) -> Result<ProjectContext> {
32    log::info!("Analyzing project context");
33    
34    let mut entry_points = Vec::new();
35    let mut ports = HashSet::new();
36    let mut env_vars = HashMap::new();
37    let mut build_scripts = Vec::new();
38    
39    // Analyze based on detected languages
40    for language in languages {
41        match language.name.as_str() {
42            "JavaScript" | "TypeScript" => {
43                analyze_node_project(project_root, &mut entry_points, &mut ports, &mut env_vars, &mut build_scripts, config)?;
44            }
45            "Python" => {
46                analyze_python_project(project_root, &mut entry_points, &mut ports, &mut env_vars, &mut build_scripts, config)?;
47            }
48            "Rust" => {
49                analyze_rust_project(project_root, &mut entry_points, &mut ports, &mut env_vars, &mut build_scripts, config)?;
50            }
51            "Go" => {
52                analyze_go_project(project_root, &mut entry_points, &mut ports, &mut env_vars, &mut build_scripts, config)?;
53            }
54            "Java" | "Kotlin" => {
55                analyze_jvm_project(project_root, &mut entry_points, &mut ports, &mut env_vars, &mut build_scripts, config)?;
56            }
57            _ => {}
58        }
59    }
60    
61    // Analyze common configuration files
62    analyze_docker_files(project_root, &mut ports, &mut env_vars)?;
63    analyze_env_files(project_root, &mut env_vars)?;
64    analyze_makefile(project_root, &mut build_scripts)?;
65    
66    // Technology-specific analysis
67    for technology in technologies {
68        analyze_technology_specifics(technology, project_root, &mut entry_points, &mut ports)?;
69    }
70    
71    // Detect microservices structure
72    let microservices = detect_microservices_structure(project_root)?;
73    
74    // Determine project type
75    let ports_vec: Vec<Port> = ports.iter().cloned().collect();
76    let project_type = determine_project_type_with_structure(
77        languages, 
78        technologies, 
79        &entry_points, 
80        &ports_vec, 
81        &microservices
82    );
83    
84    // Convert collections to vectors
85    let ports: Vec<Port> = ports.into_iter().collect();
86    let environment_variables: Vec<EnvVar> = env_vars.into_iter()
87        .map(|(name, (default, required, desc))| EnvVar {
88            name,
89            default_value: default,
90            required,
91            description: desc,
92        })
93        .collect();
94    
95    Ok(ProjectContext {
96        entry_points,
97        ports,
98        environment_variables,
99        project_type,
100        build_scripts,
101    })
102}
103
104/// Represents a detected microservice within the project
105#[derive(Debug)]
106struct MicroserviceInfo {
107    name: String,
108    has_db: bool,
109    has_api: bool,
110}
111
112/// Detects microservice structure based on directory patterns
113fn detect_microservices_structure(project_root: &Path) -> Result<Vec<MicroserviceInfo>> {
114    let mut microservices = Vec::new();
115    
116    // Common patterns for microservice directories
117    let service_indicators = ["api", "service", "encore.service.ts", "main.ts", "main.go", "main.py"];
118    let db_indicators = ["db", "database", "migrations", "schema", "models"];
119    
120    // Check root-level directories
121    if let Ok(entries) = std::fs::read_dir(project_root) {
122        for entry in entries.flatten() {
123            if entry.file_type()?.is_dir() {
124                let dir_name = entry.file_name().to_string_lossy().to_string();
125                let dir_path = entry.path();
126                
127                // Skip common non-service directories
128                if dir_name.starts_with('.') || 
129                   ["node_modules", "target", "dist", "build", "__pycache__", "vendor"].contains(&dir_name.as_str()) {
130                    continue;
131                }
132                
133                // Check if this directory looks like a service
134                let mut has_api = false;
135                let mut has_db = false;
136                
137                if let Ok(sub_entries) = std::fs::read_dir(&dir_path) {
138                    for sub_entry in sub_entries.flatten() {
139                        let sub_name = sub_entry.file_name().to_string_lossy().to_string();
140                        
141                        // Check for API indicators
142                        if service_indicators.iter().any(|&ind| sub_name.contains(ind)) {
143                            has_api = true;
144                        }
145                        
146                        // Check for DB indicators
147                        if db_indicators.iter().any(|&ind| sub_name.contains(ind)) {
148                            has_db = true;
149                        }
150                    }
151                }
152                
153                // If it has service characteristics, add it as a microservice
154                if has_api || has_db {
155                    microservices.push(MicroserviceInfo {
156                        name: dir_name,
157                        has_db,
158                        has_api,
159                    });
160                }
161            }
162        }
163    }
164    
165    Ok(microservices)
166}
167
168/// Enhanced project type determination including microservice structure analysis
169fn determine_project_type_with_structure(
170    languages: &[DetectedLanguage],
171    technologies: &[DetectedTechnology],
172    entry_points: &[EntryPoint],
173    ports: &[Port],
174    microservices: &[MicroserviceInfo],
175) -> ProjectType {
176    // If we have multiple services with databases, it's likely a microservice architecture
177    let services_with_db = microservices.iter().filter(|s| s.has_db).count();
178    if services_with_db >= 2 || microservices.len() >= 3 {
179        return ProjectType::Microservice;
180    }
181    
182    // Fall back to original determination logic
183    determine_project_type(languages, technologies, entry_points, ports)
184}
185
186/// Analyzes Node.js/JavaScript/TypeScript projects
187fn analyze_node_project(
188    root: &Path,
189    entry_points: &mut Vec<EntryPoint>,
190    ports: &mut HashSet<Port>,
191    env_vars: &mut HashMap<String, (Option<String>, bool, Option<String>)>,
192    build_scripts: &mut Vec<BuildScript>,
193    config: &AnalysisConfig,
194) -> Result<()> {
195    let package_json_path = root.join("package.json");
196    
197    if is_readable_file(&package_json_path) {
198        let content = read_file_safe(&package_json_path, config.max_file_size)?;
199        let package_json: Value = serde_json::from_str(&content)?;
200        
201        // Extract scripts
202        if let Some(scripts) = package_json.get("scripts").and_then(|s| s.as_object()) {
203            for (name, command) in scripts {
204                if let Some(cmd) = command.as_str() {
205                    build_scripts.push(BuildScript {
206                        name: name.clone(),
207                        command: cmd.to_string(),
208                        description: get_script_description(name),
209                        is_default: name == "start" || name == "dev",
210                    });
211                    
212                    // Look for ports in scripts
213                    extract_ports_from_command(cmd, ports);
214                }
215            }
216        }
217        
218        // Find main entry point
219        if let Some(main) = package_json.get("main").and_then(|m| m.as_str()) {
220            entry_points.push(EntryPoint {
221                file: root.join(main),
222                function: None,
223                command: Some(format!("node {}", main)),
224            });
225        }
226        
227        // Check common entry files
228        let common_entries = ["index.js", "index.ts", "app.js", "app.ts", "server.js", "server.ts", "main.js", "main.ts"];
229        for entry in &common_entries {
230            let path = root.join(entry);
231            if is_readable_file(&path) {
232                scan_js_file_for_context(&path, entry_points, ports, env_vars, config)?;
233            }
234        }
235        
236        // Check src directory
237        let src_dir = root.join("src");
238        if src_dir.is_dir() {
239            for entry in &common_entries {
240                let path = src_dir.join(entry);
241                if is_readable_file(&path) {
242                    scan_js_file_for_context(&path, entry_points, ports, env_vars, config)?;
243                }
244            }
245        }
246    }
247    
248    Ok(())
249}
250
251/// Scans JavaScript/TypeScript files for context information
252fn scan_js_file_for_context(
253    path: &Path,
254    entry_points: &mut Vec<EntryPoint>,
255    ports: &mut HashSet<Port>,
256    env_vars: &mut HashMap<String, (Option<String>, bool, Option<String>)>,
257    config: &AnalysisConfig,
258) -> Result<()> {
259    let content = read_file_safe(path, config.max_file_size)?;
260    
261    // Look for port assignments
262    let port_regex = Regex::new(r"(?:PORT|port)\s*[=:]\s*(?:process\.env\.PORT\s*\|\|\s*)?(\d{1,5})")
263        .map_err(|e| AnalysisError::InvalidStructure(format!("Invalid regex: {}", e)))?;
264    for cap in port_regex.captures_iter(&content) {
265        if let Some(port_str) = cap.get(1) {
266            if let Ok(port) = port_str.as_str().parse::<u16>() {
267                ports.insert(Port {
268                    number: port,
269                    protocol: Protocol::Http,
270                    description: Some("HTTP server port".to_string()),
271                });
272            }
273        }
274    }
275    
276    // Look for app.listen() calls
277    let listen_regex = Regex::new(r"\.listen\s*\(\s*(\d{1,5})")
278        .map_err(|e| AnalysisError::InvalidStructure(format!("Invalid regex: {}", e)))?;
279    for cap in listen_regex.captures_iter(&content) {
280        if let Some(port_str) = cap.get(1) {
281            if let Ok(port) = port_str.as_str().parse::<u16>() {
282                ports.insert(Port {
283                    number: port,
284                    protocol: Protocol::Http,
285                    description: Some("Express/HTTP server".to_string()),
286                });
287            }
288        }
289    }
290    
291    // Look for environment variable usage
292    let env_regex = Regex::new(r"process\.env\.([A-Z_][A-Z0-9_]*)")
293        .map_err(|e| AnalysisError::InvalidStructure(format!("Invalid regex: {}", e)))?;
294    for cap in env_regex.captures_iter(&content) {
295        if let Some(var_name) = cap.get(1) {
296            let name = var_name.as_str().to_string();
297            if !name.starts_with("NODE_") { // Skip Node.js internal vars
298                env_vars.entry(name.clone()).or_insert((None, false, None));
299            }
300        }
301    }
302    
303    // Look for Encore.dev imports and patterns
304    if content.contains("encore.dev") {
305        // Encore uses specific patterns for config and database
306        let encore_patterns = [
307            (r#"secret\s*\(\s*['"]([A-Z_][A-Z0-9_]*)['"]"#, "Encore secret configuration"),
308            (r#"SQLDatabase\s*\(\s*['"](\w+)['"]"#, "Encore database"),
309        ];
310        
311        for (pattern, description) in &encore_patterns {
312            let regex = Regex::new(pattern).unwrap_or_else(|_| Regex::new(r"").unwrap());
313            for cap in regex.captures_iter(&content) {
314                if let Some(match_str) = cap.get(1) {
315                    let name = match_str.as_str();
316                    if pattern.contains("secret") {
317                        env_vars.entry(name.to_string())
318                            .or_insert((None, true, Some(description.to_string())));
319                    }
320                }
321            }
322        }
323    }
324    
325    // Check if this is a main entry point
326    if content.contains("createServer") || content.contains(".listen(") || 
327       content.contains("app.listen") || content.contains("server.listen") ||
328       content.contains("encore.dev") && content.contains("api.") {
329        entry_points.push(EntryPoint {
330            file: path.to_path_buf(),
331            function: Some("main".to_string()),
332            command: Some(format!("node {}", path.file_name().unwrap().to_string_lossy())),
333        });
334    }
335    
336    Ok(())
337}
338
339/// Analyzes Python projects
340fn analyze_python_project(
341    root: &Path,
342    entry_points: &mut Vec<EntryPoint>,
343    ports: &mut HashSet<Port>,
344    env_vars: &mut HashMap<String, (Option<String>, bool, Option<String>)>,
345    build_scripts: &mut Vec<BuildScript>,
346    config: &AnalysisConfig,
347) -> Result<()> {
348    // Check for common Python entry points
349    let common_entries = ["main.py", "app.py", "wsgi.py", "asgi.py", "manage.py", "run.py", "__main__.py"];
350    
351    for entry in &common_entries {
352        let path = root.join(entry);
353        if is_readable_file(&path) {
354            scan_python_file_for_context(&path, entry_points, ports, env_vars, config)?;
355        }
356    }
357    
358    // Check setup.py for entry points
359    let setup_py = root.join("setup.py");
360    if is_readable_file(&setup_py) {
361        let content = read_file_safe(&setup_py, config.max_file_size)?;
362        
363        // Look for console_scripts
364        let console_regex = create_regex(r#"console_scripts['"]\s*:\s*\[(.*?)\]"#)?;
365        if let Some(cap) = console_regex.captures(&content) {
366            if let Some(scripts) = cap.get(1) {
367                let script_regex = create_regex(r#"['"](\w+)\s*=\s*([\w\.]+):(\w+)"#)?;
368                for script_cap in script_regex.captures_iter(scripts.as_str()) {
369                    if let (Some(name), Some(module), Some(func)) = 
370                        (script_cap.get(1), script_cap.get(2), script_cap.get(3)) {
371                        entry_points.push(EntryPoint {
372                            file: PathBuf::from(format!("{}.py", module.as_str().replace('.', "/"))),
373                            function: Some(func.as_str().to_string()),
374                            command: Some(name.as_str().to_string()),
375                        });
376                    }
377                }
378            }
379        }
380    }
381    
382    // Check pyproject.toml for scripts
383    let pyproject = root.join("pyproject.toml");
384    if is_readable_file(&pyproject) {
385        let content = read_file_safe(&pyproject, config.max_file_size)?;
386        if let Ok(toml_value) = toml::from_str::<toml::Value>(&content) {
387            // Extract build scripts from poetry
388            if let Some(scripts) = toml_value.get("tool")
389                .and_then(|t| t.get("poetry"))
390                .and_then(|p| p.get("scripts"))
391                .and_then(|s| s.as_table()) {
392                for (name, cmd) in scripts {
393                    if let Some(command) = cmd.as_str() {
394                        build_scripts.push(BuildScript {
395                            name: name.clone(),
396                            command: command.to_string(),
397                            description: None,
398                            is_default: name == "start" || name == "run",
399                        });
400                    }
401                }
402            }
403        }
404    }
405    
406    // Common Python build commands
407    build_scripts.push(BuildScript {
408        name: "install".to_string(),
409        command: "pip install -r requirements.txt".to_string(),
410        description: Some("Install dependencies".to_string()),
411        is_default: false,
412    });
413    
414    Ok(())
415}
416
417/// Scans Python files for context information
418fn scan_python_file_for_context(
419    path: &Path,
420    entry_points: &mut Vec<EntryPoint>,
421    ports: &mut HashSet<Port>,
422    env_vars: &mut HashMap<String, (Option<String>, bool, Option<String>)>,
423    config: &AnalysisConfig,
424) -> Result<()> {
425    let content = read_file_safe(path, config.max_file_size)?;
426    
427    // Look for Flask/FastAPI/Django port configurations
428    let port_patterns = [
429        r"port\s*=\s*(\d{1,5})",
430        r"PORT\s*=\s*(\d{1,5})",
431        r"\.run\s*\([^)]*port\s*=\s*(\d{1,5})",
432        r"uvicorn\.run\s*\([^)]*port\s*=\s*(\d{1,5})",
433    ];
434    
435    for pattern in &port_patterns {
436        let regex = create_regex(pattern)?;
437        for cap in regex.captures_iter(&content) {
438            if let Some(port_str) = cap.get(1) {
439                if let Ok(port) = port_str.as_str().parse::<u16>() {
440                    ports.insert(Port {
441                        number: port,
442                        protocol: Protocol::Http,
443                        description: Some("Python web server".to_string()),
444                    });
445                }
446            }
447        }
448    }
449    
450    // Look for environment variable usage
451    let env_patterns = [
452        r#"os\.environ\.get\s*\(\s*["']([A-Z_][A-Z0-9_]*)["']"#,
453        r#"os\.environ\s*\[\s*["']([A-Z_][A-Z0-9_]*)["']\s*\]"#,
454        r#"os\.getenv\s*\(\s*["']([A-Z_][A-Z0-9_]*)["']"#,
455    ];
456    
457    for pattern in &env_patterns {
458        let regex = create_regex(pattern)?;
459        for cap in regex.captures_iter(&content) {
460            if let Some(var_name) = cap.get(1) {
461                let name = var_name.as_str().to_string();
462                env_vars.entry(name.clone()).or_insert((None, false, None));
463            }
464        }
465    }
466    
467    // Check if this is a main entry point
468    if content.contains("if __name__ == '__main__':") ||
469       content.contains("if __name__ == \"__main__\":") {
470        entry_points.push(EntryPoint {
471            file: path.to_path_buf(),
472            function: Some("main".to_string()),
473            command: Some(format!("python {}", path.file_name().unwrap().to_string_lossy())),
474        });
475    }
476    
477    Ok(())
478}
479
480/// Analyzes Rust projects
481fn analyze_rust_project(
482    root: &Path,
483    entry_points: &mut Vec<EntryPoint>,
484    ports: &mut HashSet<Port>,
485    env_vars: &mut HashMap<String, (Option<String>, bool, Option<String>)>,
486    build_scripts: &mut Vec<BuildScript>,
487    config: &AnalysisConfig,
488) -> Result<()> {
489    let cargo_toml = root.join("Cargo.toml");
490    
491    if is_readable_file(&cargo_toml) {
492        let content = read_file_safe(&cargo_toml, config.max_file_size)?;
493        if let Ok(toml_value) = toml::from_str::<toml::Value>(&content) {
494            // Check for binary targets
495            if let Some(bins) = toml_value.get("bin").and_then(|b| b.as_array()) {
496                for bin in bins {
497                    if let Some(name) = bin.get("name").and_then(|n| n.as_str()) {
498                        let path = bin.get("path")
499                            .and_then(|p| p.as_str())
500                            .map(PathBuf::from)
501                            .unwrap_or_else(|| root.join("src").join("bin").join(format!("{}.rs", name)));
502                        
503                        entry_points.push(EntryPoint {
504                            file: path,
505                            function: Some("main".to_string()),
506                            command: Some(format!("cargo run --bin {}", name)),
507                        });
508                    }
509                }
510            }
511            
512            // Default binary
513            if let Some(_package_name) = toml_value.get("package")
514                .and_then(|p| p.get("name"))
515                .and_then(|n| n.as_str()) {
516                let main_rs = root.join("src").join("main.rs");
517                if is_readable_file(&main_rs) {
518                    entry_points.push(EntryPoint {
519                        file: main_rs.clone(),
520                        function: Some("main".to_string()),
521                        command: Some("cargo run".to_string()),
522                    });
523                    
524                    // Scan main.rs for context
525                    scan_rust_file_for_context(&main_rs, ports, env_vars, config)?;
526                }
527            }
528        }
529    }
530    
531    // Common Rust build commands
532    build_scripts.extend(vec![
533        BuildScript {
534            name: "build".to_string(),
535            command: "cargo build".to_string(),
536            description: Some("Build the project".to_string()),
537            is_default: false,
538        },
539        BuildScript {
540            name: "build-release".to_string(),
541            command: "cargo build --release".to_string(),
542            description: Some("Build optimized release version".to_string()),
543            is_default: false,
544        },
545        BuildScript {
546            name: "test".to_string(),
547            command: "cargo test".to_string(),
548            description: Some("Run tests".to_string()),
549            is_default: false,
550        },
551        BuildScript {
552            name: "run".to_string(),
553            command: "cargo run".to_string(),
554            description: Some("Run the application".to_string()),
555            is_default: true,
556        },
557    ]);
558    
559    Ok(())
560}
561
562/// Scans Rust files for context information
563fn scan_rust_file_for_context(
564    path: &Path,
565    ports: &mut HashSet<Port>,
566    env_vars: &mut HashMap<String, (Option<String>, bool, Option<String>)>,
567    config: &AnalysisConfig,
568) -> Result<()> {
569    let content = read_file_safe(path, config.max_file_size)?;
570    
571    // Look for port bindings
572    let port_patterns = [
573        r#"bind\s*\(\s*"[^"]*:(\d{1,5})"\s*\)"#,
574        r#"bind\s*\(\s*\([^,]+,\s*(\d{1,5})\)\s*\)"#,
575        r#"listen\s*\(\s*"[^"]*:(\d{1,5})"\s*\)"#,
576        r"PORT[^=]*=\s*(\d{1,5})",
577    ];
578    
579    for pattern in &port_patterns {
580        let regex = create_regex(pattern)?;
581        for cap in regex.captures_iter(&content) {
582            if let Some(port_str) = cap.get(1) {
583                if let Ok(port) = port_str.as_str().parse::<u16>() {
584                    ports.insert(Port {
585                        number: port,
586                        protocol: Protocol::Http,
587                        description: Some("Rust web server".to_string()),
588                    });
589                }
590            }
591        }
592    }
593    
594    // Look for environment variable usage
595    let env_patterns = [
596        r#"env::var\s*\(\s*"([A-Z_][A-Z0-9_]*)"\s*\)"#,
597        r#"std::env::var\s*\(\s*"([A-Z_][A-Z0-9_]*)"\s*\)"#,
598        r#"env!\s*\(\s*"([A-Z_][A-Z0-9_]*)"\s*\)"#,
599    ];
600    
601    for pattern in &env_patterns {
602        let regex = create_regex(pattern)?;
603        for cap in regex.captures_iter(&content) {
604            if let Some(var_name) = cap.get(1) {
605                let name = var_name.as_str().to_string();
606                env_vars.entry(name.clone()).or_insert((None, false, None));
607            }
608        }
609    }
610    
611    Ok(())
612}
613
614/// Analyzes Go projects
615fn analyze_go_project(
616    root: &Path,
617    entry_points: &mut Vec<EntryPoint>,
618    ports: &mut HashSet<Port>,
619    env_vars: &mut HashMap<String, (Option<String>, bool, Option<String>)>,
620    build_scripts: &mut Vec<BuildScript>,
621    config: &AnalysisConfig,
622) -> Result<()> {
623    // Check for main.go
624    let main_go = root.join("main.go");
625    if is_readable_file(&main_go) {
626        entry_points.push(EntryPoint {
627            file: main_go.clone(),
628            function: Some("main".to_string()),
629            command: Some("go run main.go".to_string()),
630        });
631        
632        scan_go_file_for_context(&main_go, ports, env_vars, config)?;
633    }
634    
635    // Check cmd directory for multiple binaries
636    let cmd_dir = root.join("cmd");
637    if cmd_dir.is_dir() {
638        if let Ok(entries) = std::fs::read_dir(&cmd_dir) {
639            for entry in entries.flatten() {
640                if entry.file_type()?.is_dir() {
641                    let main_file = entry.path().join("main.go");
642                    if is_readable_file(&main_file) {
643                        let cmd_name = entry.file_name().to_string_lossy().to_string();
644                        entry_points.push(EntryPoint {
645                            file: main_file.clone(),
646                            function: Some("main".to_string()),
647                            command: Some(format!("go run ./cmd/{}", cmd_name)),
648                        });
649                        
650                        scan_go_file_for_context(&main_file, ports, env_vars, config)?;
651                    }
652                }
653            }
654        }
655    }
656    
657    // Common Go build commands
658    build_scripts.extend(vec![
659        BuildScript {
660            name: "build".to_string(),
661            command: "go build".to_string(),
662            description: Some("Build the project".to_string()),
663            is_default: false,
664        },
665        BuildScript {
666            name: "test".to_string(),
667            command: "go test ./...".to_string(),
668            description: Some("Run tests".to_string()),
669            is_default: false,
670        },
671        BuildScript {
672            name: "run".to_string(),
673            command: "go run .".to_string(),
674            description: Some("Run the application".to_string()),
675            is_default: true,
676        },
677        BuildScript {
678            name: "mod-download".to_string(),
679            command: "go mod download".to_string(),
680            description: Some("Download dependencies".to_string()),
681            is_default: false,
682        },
683    ]);
684    
685    Ok(())
686}
687
688/// Scans Go files for context information
689fn scan_go_file_for_context(
690    path: &Path,
691    ports: &mut HashSet<Port>,
692    env_vars: &mut HashMap<String, (Option<String>, bool, Option<String>)>,
693    config: &AnalysisConfig,
694) -> Result<()> {
695    let content = read_file_safe(path, config.max_file_size)?;
696    
697    // Look for port bindings
698    let port_patterns = [
699        r#"Listen\s*\(\s*":(\d{1,5})"\s*\)"#,
700        r#"ListenAndServe\s*\(\s*":(\d{1,5})"\s*,"#,
701        r#"Addr:\s*":(\d{1,5})""#,
702        r"PORT[^=]*=\s*(\d{1,5})",
703    ];
704    
705    for pattern in &port_patterns {
706        let regex = create_regex(pattern)?;
707        for cap in regex.captures_iter(&content) {
708            if let Some(port_str) = cap.get(1) {
709                if let Ok(port) = port_str.as_str().parse::<u16>() {
710                    ports.insert(Port {
711                        number: port,
712                        protocol: Protocol::Http,
713                        description: Some("Go web server".to_string()),
714                    });
715                }
716            }
717        }
718    }
719    
720    // Look for environment variable usage
721    let env_patterns = [
722        r#"os\.Getenv\s*\(\s*"([A-Z_][A-Z0-9_]*)"\s*\)"#,
723        r#"os\.LookupEnv\s*\(\s*"([A-Z_][A-Z0-9_]*)"\s*\)"#,
724    ];
725    
726    for pattern in &env_patterns {
727        let regex = create_regex(pattern)?;
728        for cap in regex.captures_iter(&content) {
729            if let Some(var_name) = cap.get(1) {
730                let name = var_name.as_str().to_string();
731                env_vars.entry(name.clone()).or_insert((None, false, None));
732            }
733        }
734    }
735    
736    Ok(())
737}
738
739/// Analyzes JVM projects (Java/Kotlin)
740fn analyze_jvm_project(
741    root: &Path,
742    _entry_points: &mut Vec<EntryPoint>,
743    ports: &mut HashSet<Port>,
744    env_vars: &mut HashMap<String, (Option<String>, bool, Option<String>)>,
745    build_scripts: &mut Vec<BuildScript>,
746    config: &AnalysisConfig,
747) -> Result<()> {
748    // Check for Maven
749    let pom_xml = root.join("pom.xml");
750    if is_readable_file(&pom_xml) {
751        build_scripts.extend(vec![
752            BuildScript {
753                name: "build".to_string(),
754                command: "mvn clean package".to_string(),
755                description: Some("Build with Maven".to_string()),
756                is_default: false,
757            },
758            BuildScript {
759                name: "test".to_string(),
760                command: "mvn test".to_string(),
761                description: Some("Run tests".to_string()),
762                is_default: false,
763            },
764            BuildScript {
765                name: "run".to_string(),
766                command: "mvn spring-boot:run".to_string(),
767                description: Some("Run Spring Boot application".to_string()),
768                is_default: true,
769            },
770        ]);
771    }
772    
773    // Check for Gradle
774    let gradle_files = ["build.gradle", "build.gradle.kts"];
775    for gradle_file in &gradle_files {
776        if is_readable_file(&root.join(gradle_file)) {
777            build_scripts.extend(vec![
778                BuildScript {
779                    name: "build".to_string(),
780                    command: "./gradlew build".to_string(),
781                    description: Some("Build with Gradle".to_string()),
782                    is_default: false,
783                },
784                BuildScript {
785                    name: "test".to_string(),
786                    command: "./gradlew test".to_string(),
787                    description: Some("Run tests".to_string()),
788                    is_default: false,
789                },
790                BuildScript {
791                    name: "run".to_string(),
792                    command: "./gradlew bootRun".to_string(),
793                    description: Some("Run Spring Boot application".to_string()),
794                    is_default: true,
795                },
796            ]);
797            break;
798        }
799    }
800    
801    // Look for application properties
802    let app_props_locations = [
803        "src/main/resources/application.properties",
804        "src/main/resources/application.yml",
805        "src/main/resources/application.yaml",
806    ];
807    
808    for props_path in &app_props_locations {
809        let full_path = root.join(props_path);
810        if is_readable_file(&full_path) {
811            analyze_application_properties(&full_path, ports, env_vars, config)?;
812        }
813    }
814    
815    Ok(())
816}
817
818/// Analyzes application properties files
819fn analyze_application_properties(
820    path: &Path,
821    ports: &mut HashSet<Port>,
822    env_vars: &mut HashMap<String, (Option<String>, bool, Option<String>)>,
823    config: &AnalysisConfig,
824) -> Result<()> {
825    let content = read_file_safe(path, config.max_file_size)?;
826    
827    // Look for server.port
828    let port_regex = create_regex(r"server\.port\s*[=:]\s*(\d{1,5})")?;
829    for cap in port_regex.captures_iter(&content) {
830        if let Some(port_str) = cap.get(1) {
831            if let Ok(port) = port_str.as_str().parse::<u16>() {
832                ports.insert(Port {
833                    number: port,
834                    protocol: Protocol::Http,
835                    description: Some("Spring Boot server".to_string()),
836                });
837            }
838        }
839    }
840    
841    // Look for ${ENV_VAR} placeholders
842    let env_regex = create_regex(r"\$\{([A-Z_][A-Z0-9_]*)\}")?;
843    for cap in env_regex.captures_iter(&content) {
844        if let Some(var_name) = cap.get(1) {
845            let name = var_name.as_str().to_string();
846            env_vars.entry(name.clone()).or_insert((None, false, None));
847        }
848    }
849    
850    Ok(())
851}
852
853/// Analyzes Docker files for ports and environment variables
854fn analyze_docker_files(
855    root: &Path,
856    ports: &mut HashSet<Port>,
857    env_vars: &mut HashMap<String, (Option<String>, bool, Option<String>)>,
858) -> Result<()> {
859    let dockerfile = root.join("Dockerfile");
860    
861    if is_readable_file(&dockerfile) {
862        let content = std::fs::read_to_string(&dockerfile)?;
863        
864        // Look for EXPOSE directives
865        let expose_regex = create_regex(r"EXPOSE\s+(\d{1,5})(?:/(\w+))?")?;
866        for cap in expose_regex.captures_iter(&content) {
867            if let Some(port_str) = cap.get(1) {
868                if let Ok(port) = port_str.as_str().parse::<u16>() {
869                    let protocol = cap.get(2)
870                        .and_then(|p| match p.as_str().to_lowercase().as_str() {
871                            "tcp" => Some(Protocol::Tcp),
872                            "udp" => Some(Protocol::Udp),
873                            _ => None,
874                        })
875                        .unwrap_or(Protocol::Tcp);
876                    
877                    ports.insert(Port {
878                        number: port,
879                        protocol,
880                        description: Some("Exposed in Dockerfile".to_string()),
881                    });
882                }
883            }
884        }
885        
886        // Look for ENV directives
887        let env_regex = create_regex(r"ENV\s+([A-Z_][A-Z0-9_]*)\s+(.+)")?;
888        for cap in env_regex.captures_iter(&content) {
889            if let (Some(name), Some(value)) = (cap.get(1), cap.get(2)) {
890                let var_name = name.as_str().to_string();
891                let var_value = value.as_str().trim().to_string();
892                env_vars.entry(var_name).or_insert((Some(var_value), false, None));
893            }
894        }
895    }
896    
897    // Check docker-compose files
898    let compose_files = ["docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"];
899    for compose_file in &compose_files {
900        let path = root.join(compose_file);
901        if is_readable_file(&path) {
902            analyze_docker_compose(&path, ports, env_vars)?;
903            break;
904        }
905    }
906    
907    Ok(())
908}
909
910/// Analyzes docker-compose files
911fn analyze_docker_compose(
912    path: &Path,
913    ports: &mut HashSet<Port>,
914    env_vars: &mut HashMap<String, (Option<String>, bool, Option<String>)>,
915) -> Result<()> {
916    let content = std::fs::read_to_string(path)?;
917    let value: serde_yaml::Value = serde_yaml::from_str(&content).map_err(|e| AnalysisError::InvalidStructure(format!("Invalid YAML: {}", e)))?;
918    
919    if let Some(services) = value.get("services").and_then(|s| s.as_mapping()) {
920        for (service_name, service) in services {
921            let service_name_str = service_name.as_str().unwrap_or("unknown");
922            
923            // Determine service type based on image, name, and other indicators
924            let service_type = determine_service_type(service_name_str, service);
925            
926            // Extract ports
927            if let Some(service_ports) = service.get("ports").and_then(|p| p.as_sequence()) {
928                for port_entry in service_ports {
929                    if let Some(port_str) = port_entry.as_str() {
930                        // Parse port mappings like "8080:80" or just "80"
931                        let parts: Vec<&str> = port_str.split(':').collect();
932                        
933                        let (external_port, internal_port, protocol_suffix) = if parts.len() >= 2 {
934                            // Format: "external:internal" or "external:internal/protocol"
935                            let external = parts[0].trim();
936                            let internal_parts: Vec<&str> = parts[1].split('/').collect();
937                            let internal = internal_parts[0].trim();
938                            let protocol = internal_parts.get(1).map(|p| p.trim());
939                            (external, internal, protocol)
940                        } else {
941                            // Format: just "port" or "port/protocol"
942                            let port_parts: Vec<&str> = parts[0].split('/').collect();
943                            let port = port_parts[0].trim();
944                            let protocol = port_parts.get(1).map(|p| p.trim());
945                            (port, port, protocol)
946                        };
947                        
948                        // Determine protocol
949                        let protocol = match protocol_suffix {
950                            Some("udp") => Protocol::Udp,
951                            _ => Protocol::Tcp,
952                        };
953                        
954                        // Create descriptive port entry
955                        if let Ok(port) = external_port.parse::<u16>() {
956                            let description = create_port_description(&service_type, service_name_str, external_port, internal_port);
957                            
958                            ports.insert(Port {
959                                number: port,
960                                protocol,
961                                description: Some(description),
962                            });
963                        }
964                    }
965                }
966            }
967            
968            // Extract environment variables with context
969            if let Some(env) = service.get("environment") {
970                let env_context = format!(" ({})", service_type.as_str());
971                
972                if let Some(env_map) = env.as_mapping() {
973                    for (key, value) in env_map {
974                        if let Some(key_str) = key.as_str() {
975                            let val_str = value.as_str().map(|s| s.to_string());
976                            let description = get_env_var_description(key_str, &service_type);
977                            env_vars.entry(key_str.to_string())
978                                .or_insert((val_str, false, description.or_else(|| Some(env_context.clone()))));
979                        }
980                    }
981                } else if let Some(env_list) = env.as_sequence() {
982                    for item in env_list {
983                        if let Some(env_str) = item.as_str() {
984                            if let Some(eq_pos) = env_str.find('=') {
985                                let (key, value) = env_str.split_at(eq_pos);
986                                let value = &value[1..]; // Skip the '='
987                                let description = get_env_var_description(key, &service_type);
988                                env_vars.entry(key.to_string())
989                                    .or_insert((Some(value.to_string()), false, description.or_else(|| Some(env_context.clone()))));
990                            }
991                        }
992                    }
993                }
994            }
995        }
996    }
997    
998    Ok(())
999}
1000
1001/// Service types found in Docker Compose
1002#[derive(Debug, Clone)]
1003enum ServiceType {
1004    PostgreSQL,
1005    MySQL,
1006    MongoDB,
1007    Redis,
1008    RabbitMQ,
1009    Kafka,
1010    Elasticsearch,
1011    Application,
1012    Nginx,
1013    Unknown,
1014}
1015
1016impl ServiceType {
1017    fn as_str(&self) -> &'static str {
1018        match self {
1019            ServiceType::PostgreSQL => "PostgreSQL database",
1020            ServiceType::MySQL => "MySQL database",
1021            ServiceType::MongoDB => "MongoDB database",
1022            ServiceType::Redis => "Redis cache",
1023            ServiceType::RabbitMQ => "RabbitMQ message broker",
1024            ServiceType::Kafka => "Kafka message broker",
1025            ServiceType::Elasticsearch => "Elasticsearch search engine",
1026            ServiceType::Application => "Application service",
1027            ServiceType::Nginx => "Nginx web server",
1028            ServiceType::Unknown => "Service",
1029        }
1030    }
1031}
1032
1033/// Determines the type of service based on various indicators
1034fn determine_service_type(name: &str, service: &serde_yaml::Value) -> ServiceType {
1035    let name_lower = name.to_lowercase();
1036    
1037    // Check service name
1038    if name_lower.contains("postgres") || name_lower.contains("pg") || name_lower.contains("psql") {
1039        return ServiceType::PostgreSQL;
1040    } else if name_lower.contains("mysql") || name_lower.contains("mariadb") {
1041        return ServiceType::MySQL;
1042    } else if name_lower.contains("mongo") {
1043        return ServiceType::MongoDB;
1044    } else if name_lower.contains("redis") {
1045        return ServiceType::Redis;
1046    } else if name_lower.contains("rabbit") || name_lower.contains("amqp") {
1047        return ServiceType::RabbitMQ;
1048    } else if name_lower.contains("kafka") {
1049        return ServiceType::Kafka;
1050    } else if name_lower.contains("elastic") || name_lower.contains("es") {
1051        return ServiceType::Elasticsearch;
1052    } else if name_lower.contains("nginx") || name_lower.contains("proxy") {
1053        return ServiceType::Nginx;
1054    }
1055    
1056    // Check image name
1057    if let Some(image) = service.get("image").and_then(|i| i.as_str()) {
1058        let image_lower = image.to_lowercase();
1059        if image_lower.contains("postgres") {
1060            return ServiceType::PostgreSQL;
1061        } else if image_lower.contains("mysql") || image_lower.contains("mariadb") {
1062            return ServiceType::MySQL;
1063        } else if image_lower.contains("mongo") {
1064            return ServiceType::MongoDB;
1065        } else if image_lower.contains("redis") {
1066            return ServiceType::Redis;
1067        } else if image_lower.contains("rabbitmq") {
1068            return ServiceType::RabbitMQ;
1069        } else if image_lower.contains("kafka") {
1070            return ServiceType::Kafka;
1071        } else if image_lower.contains("elastic") {
1072            return ServiceType::Elasticsearch;
1073        } else if image_lower.contains("nginx") {
1074            return ServiceType::Nginx;
1075        }
1076    }
1077    
1078    // Check environment variables for clues
1079    if let Some(env) = service.get("environment") {
1080        if let Some(env_map) = env.as_mapping() {
1081            for (key, _) in env_map {
1082                if let Some(key_str) = key.as_str() {
1083                    if key_str.contains("POSTGRES") || key_str.contains("PGPASSWORD") {
1084                        return ServiceType::PostgreSQL;
1085                    } else if key_str.contains("MYSQL") {
1086                        return ServiceType::MySQL;
1087                    } else if key_str.contains("MONGO") {
1088                        return ServiceType::MongoDB;
1089                    }
1090                }
1091            }
1092        }
1093    }
1094    
1095    // Check if it has a build context (likely application)
1096    if service.get("build").is_some() {
1097        return ServiceType::Application;
1098    }
1099    
1100    ServiceType::Unknown
1101}
1102
1103/// Creates a descriptive port description based on service type
1104fn create_port_description(service_type: &ServiceType, service_name: &str, external: &str, internal: &str) -> String {
1105    let base_desc = match service_type {
1106        ServiceType::PostgreSQL => format!("PostgreSQL database ({})", service_name),
1107        ServiceType::MySQL => format!("MySQL database ({})", service_name),
1108        ServiceType::MongoDB => format!("MongoDB database ({})", service_name),
1109        ServiceType::Redis => format!("Redis cache ({})", service_name),
1110        ServiceType::RabbitMQ => format!("RabbitMQ message broker ({})", service_name),
1111        ServiceType::Kafka => format!("Kafka message broker ({})", service_name),
1112        ServiceType::Elasticsearch => format!("Elasticsearch ({})", service_name),
1113        ServiceType::Nginx => format!("Nginx proxy ({})", service_name),
1114        ServiceType::Application => format!("Application service ({})", service_name),
1115        ServiceType::Unknown => format!("Docker service ({})", service_name),
1116    };
1117    
1118    if external != internal {
1119        format!("{} - external:{}, internal:{}", base_desc, external, internal)
1120    } else {
1121        format!("{} - port {}", base_desc, external)
1122    }
1123}
1124
1125/// Gets a descriptive context for environment variables based on service type
1126fn get_env_var_description(var_name: &str, service_type: &ServiceType) -> Option<String> {
1127    match var_name {
1128        "POSTGRES_PASSWORD" | "POSTGRES_USER" | "POSTGRES_DB" => 
1129            Some("PostgreSQL configuration".to_string()),
1130        "MYSQL_ROOT_PASSWORD" | "MYSQL_PASSWORD" | "MYSQL_USER" | "MYSQL_DATABASE" => 
1131            Some("MySQL configuration".to_string()),
1132        "MONGO_INITDB_ROOT_USERNAME" | "MONGO_INITDB_ROOT_PASSWORD" => 
1133            Some("MongoDB configuration".to_string()),
1134        "REDIS_PASSWORD" => Some("Redis configuration".to_string()),
1135        "RABBITMQ_DEFAULT_USER" | "RABBITMQ_DEFAULT_PASS" => 
1136            Some("RabbitMQ configuration".to_string()),
1137        "DATABASE_URL" | "DB_CONNECTION_STRING" => 
1138            Some("Database connection string".to_string()),
1139        "GOOGLE_APPLICATION_CREDENTIALS" => 
1140            Some("Google Cloud service account credentials".to_string()),
1141        _ => None,
1142    }
1143}
1144
1145/// Analyzes .env files
1146fn analyze_env_files(
1147    root: &Path,
1148    env_vars: &mut HashMap<String, (Option<String>, bool, Option<String>)>,
1149) -> Result<()> {
1150    let env_files = [".env", ".env.example", ".env.local", ".env.development", ".env.production"];
1151    
1152    for env_file in &env_files {
1153        let path = root.join(env_file);
1154        if is_readable_file(&path) {
1155            let content = std::fs::read_to_string(&path)?;
1156            
1157            for line in content.lines() {
1158                let line = line.trim();
1159                if line.is_empty() || line.starts_with('#') {
1160                    continue;
1161                }
1162                
1163                if let Some(eq_pos) = line.find('=') {
1164                    let (key, value) = line.split_at(eq_pos);
1165                    let key = key.trim();
1166                    let value = value[1..].trim(); // Skip the '='
1167                    
1168                    // Check if it's marked as required (common convention)
1169                    let required = value.is_empty() || value == "required" || value == "REQUIRED";
1170                    let actual_value = if required { None } else { Some(value.to_string()) };
1171                    
1172                    env_vars.entry(key.to_string()).or_insert((actual_value, required, None));
1173                }
1174            }
1175        }
1176    }
1177    
1178    Ok(())
1179}
1180
1181/// Analyzes Makefile for build scripts
1182fn analyze_makefile(
1183    root: &Path,
1184    build_scripts: &mut Vec<BuildScript>,
1185) -> Result<()> {
1186    let makefiles = ["Makefile", "makefile"];
1187    
1188    for makefile in &makefiles {
1189        let path = root.join(makefile);
1190        if is_readable_file(&path) {
1191            let content = std::fs::read_to_string(&path)?;
1192            
1193            // Simple Makefile target extraction
1194            let target_regex = create_regex(r"^([a-zA-Z0-9_-]+):\s*(?:[^\n]*)?$")?;
1195            let mut in_recipe = false;
1196            let mut current_target = String::new();
1197            let mut current_command = String::new();
1198            
1199            for line in content.lines() {
1200                if let Some(cap) = target_regex.captures(line) {
1201                    // Save previous target if any
1202                    if !current_target.is_empty() && !current_command.is_empty() {
1203                        build_scripts.push(BuildScript {
1204                            name: current_target.clone(),
1205                            command: format!("make {}", current_target),
1206                            description: None,
1207                            is_default: current_target == "run" || current_target == "start",
1208                        });
1209                    }
1210                    
1211                    if let Some(target) = cap.get(1) {
1212                        current_target = target.as_str().to_string();
1213                        current_command.clear();
1214                        in_recipe = true;
1215                    }
1216                } else if in_recipe && line.starts_with('\t') {
1217                    if current_command.is_empty() {
1218                        current_command = line.trim().to_string();
1219                    }
1220                } else if !line.trim().is_empty() {
1221                    in_recipe = false;
1222                }
1223            }
1224            
1225            // Save last target
1226            if !current_target.is_empty() && !current_command.is_empty() {
1227                build_scripts.push(BuildScript {
1228                    name: current_target.clone(),
1229                    command: format!("make {}", current_target),
1230                    description: None,
1231                    is_default: current_target == "run" || current_target == "start",
1232                });
1233            }
1234            
1235            break;
1236        }
1237    }
1238    
1239    Ok(())
1240}
1241
1242/// Analyzes technology-specific configurations
1243fn analyze_technology_specifics(
1244    technology: &DetectedTechnology,
1245    root: &Path,
1246    entry_points: &mut Vec<EntryPoint>,
1247    ports: &mut HashSet<Port>,
1248) -> Result<()> {
1249    match technology.name.as_str() {
1250        "Next.js" => {
1251            // Next.js typically runs on port 3000
1252            ports.insert(Port {
1253                number: 3000,
1254                protocol: Protocol::Http,
1255                description: Some("Next.js development server".to_string()),
1256            });
1257            
1258            // Look for pages directory
1259            let pages_dir = root.join("pages");
1260            if pages_dir.is_dir() {
1261                entry_points.push(EntryPoint {
1262                    file: pages_dir,
1263                    function: None,
1264                    command: Some("npm run dev".to_string()),
1265                });
1266            }
1267        }
1268        "Express" | "Fastify" | "Koa" | "Hono" | "Elysia" => {
1269            // Common Node.js web framework ports
1270            ports.insert(Port {
1271                number: 3000,
1272                protocol: Protocol::Http,
1273                description: Some(format!("{} server", technology.name)),
1274            });
1275        }
1276        "Encore" => {
1277            // Encore development server typically runs on port 4000
1278            ports.insert(Port {
1279                number: 4000,
1280                protocol: Protocol::Http,
1281                description: Some("Encore development server".to_string()),
1282            });
1283        }
1284        "Astro" => {
1285            // Astro development server typically runs on port 3000 or 4321
1286            ports.insert(Port {
1287                number: 4321,
1288                protocol: Protocol::Http,
1289                description: Some("Astro development server".to_string()),
1290            });
1291        }
1292        "SvelteKit" => {
1293            // SvelteKit development server typically runs on port 5173
1294            ports.insert(Port {
1295                number: 5173,
1296                protocol: Protocol::Http,
1297                description: Some("SvelteKit development server".to_string()),
1298            });
1299        }
1300        "Nuxt.js" => {
1301            // Nuxt.js development server typically runs on port 3000
1302            ports.insert(Port {
1303                number: 3000,
1304                protocol: Protocol::Http,
1305                description: Some("Nuxt.js development server".to_string()),
1306            });
1307        }
1308        "Tanstack Start" => {
1309            // Modern React framework typically runs on port 3000
1310            ports.insert(Port {
1311                number: 3000,
1312                protocol: Protocol::Http,
1313                description: Some(format!("{} development server", technology.name)),
1314            });
1315        }
1316        "React Router v7" => {
1317            // React Router v7 development server typically runs on port 5173
1318            ports.insert(Port {
1319                number: 5173,
1320                protocol: Protocol::Http,
1321                description: Some("React Router v7 development server".to_string()),
1322            });
1323        }
1324        "Django" => {
1325            ports.insert(Port {
1326                number: 8000,
1327                protocol: Protocol::Http,
1328                description: Some("Django development server".to_string()),
1329            });
1330        }
1331        "Flask" | "FastAPI" => {
1332            ports.insert(Port {
1333                number: 5000,
1334                protocol: Protocol::Http,
1335                description: Some(format!("{} server", technology.name)),
1336            });
1337        }
1338        "Spring Boot" => {
1339            ports.insert(Port {
1340                number: 8080,
1341                protocol: Protocol::Http,
1342                description: Some("Spring Boot server".to_string()),
1343            });
1344        }
1345        "Actix Web" | "Rocket" => {
1346            ports.insert(Port {
1347                number: 8080,
1348                protocol: Protocol::Http,
1349                description: Some(format!("{} server", technology.name)),
1350            });
1351        }
1352        _ => {}
1353    }
1354    
1355    Ok(())
1356}
1357
1358/// Extracts ports from command strings
1359fn extract_ports_from_command(command: &str, ports: &mut HashSet<Port>) {
1360    // Look for common port patterns in commands
1361    let patterns = [
1362        r"-p\s+(\d{1,5})",
1363        r"--port\s+(\d{1,5})",
1364        r"--port=(\d{1,5})",
1365        r"PORT=(\d{1,5})",
1366    ];
1367    
1368    for pattern in &patterns {
1369        if let Ok(regex) = Regex::new(pattern) {
1370            for cap in regex.captures_iter(command) {
1371                if let Some(port_str) = cap.get(1) {
1372                    if let Ok(port) = port_str.as_str().parse::<u16>() {
1373                        ports.insert(Port {
1374                            number: port,
1375                            protocol: Protocol::Http,
1376                            description: Some("Port from command".to_string()),
1377                        });
1378                    }
1379                }
1380            }
1381        }
1382    }
1383}
1384
1385/// Helper function to get script description
1386fn get_script_description(name: &str) -> Option<String> {
1387    match name {
1388        "start" => Some("Start the application".to_string()),
1389        "dev" => Some("Start development server".to_string()),
1390        "build" => Some("Build the application".to_string()),
1391        "test" => Some("Run tests".to_string()),
1392        "lint" => Some("Run linter".to_string()),
1393        "format" => Some("Format code".to_string()),
1394        _ => None,
1395    }
1396}
1397
1398/// Determines the project type based on analysis
1399fn determine_project_type(
1400    languages: &[DetectedLanguage],
1401    technologies: &[DetectedTechnology],
1402    entry_points: &[EntryPoint],
1403    ports: &[Port],
1404) -> ProjectType {
1405    // Check for microservice architecture indicators
1406    let has_database_ports = ports.iter().any(|p| {
1407        if let Some(desc) = &p.description {
1408            let desc_lower = desc.to_lowercase();
1409            desc_lower.contains("postgres") || desc_lower.contains("mysql") || 
1410            desc_lower.contains("mongodb") || desc_lower.contains("database")
1411        } else {
1412            false
1413        }
1414    });
1415    
1416    let has_multiple_services = ports.iter()
1417        .filter_map(|p| p.description.as_ref())
1418        .filter(|desc| {
1419            let desc_lower = desc.to_lowercase();
1420            desc_lower.contains("service") || desc_lower.contains("application")
1421        })
1422        .count() > 1;
1423    
1424    let has_orchestration_framework = technologies.iter()
1425        .any(|t| t.name == "Encore" || t.name == "Dapr" || t.name == "Temporal");
1426    
1427    // Check for web frameworks
1428    let web_frameworks = ["Express", "Fastify", "Koa", "Next.js", "React", "Vue", "Angular",
1429                         "Django", "Flask", "FastAPI", "Spring Boot", "Actix Web", "Rocket",
1430                         "Gin", "Echo", "Fiber", "Svelte", "SvelteKit", "SolidJS", "Astro",
1431                         "Encore", "Hono", "Elysia", "React Router v7", "Tanstack Start",
1432                         "SolidStart", "Qwik", "Nuxt.js", "Gatsby"];
1433    
1434    let has_web_framework = technologies.iter()
1435        .any(|t| web_frameworks.contains(&t.name.as_str()));
1436    
1437    // Check for CLI indicators
1438    let cli_indicators = ["cobra", "clap", "argparse", "commander"];
1439    let has_cli_framework = technologies.iter()
1440        .any(|t| cli_indicators.contains(&t.name.to_lowercase().as_str()));
1441    
1442    // Check for API indicators
1443    let api_frameworks = ["FastAPI", "Express", "Gin", "Echo", "Actix Web", "Spring Boot",
1444                          "Fastify", "Koa", "Nest.js", "Encore", "Hono", "Elysia"];
1445    let has_api_framework = technologies.iter()
1446        .any(|t| api_frameworks.contains(&t.name.as_str()));
1447    
1448    // Check for static site generators
1449    let static_generators = ["Gatsby", "Hugo", "Jekyll", "Eleventy", "Astro"];
1450    let has_static_generator = technologies.iter()
1451        .any(|t| static_generators.contains(&t.name.as_str()));
1452    
1453    // Determine type based on indicators
1454    if (has_database_ports || has_multiple_services) && (has_orchestration_framework || has_api_framework) {
1455        ProjectType::Microservice
1456    } else if has_static_generator {
1457        ProjectType::StaticSite
1458    } else if has_api_framework && !has_web_framework {
1459        ProjectType::ApiService
1460    } else if has_web_framework {
1461        ProjectType::WebApplication
1462    } else if has_cli_framework || (entry_points.len() == 1 && ports.is_empty()) {
1463        ProjectType::CliTool
1464    } else if entry_points.is_empty() && ports.is_empty() {
1465        // Check if it's a library
1466        let has_lib_indicators = languages.iter().any(|l| {
1467            match l.name.as_str() {
1468                "Rust" => l.files.iter().any(|f| f.to_string_lossy().contains("lib.rs")),
1469                "Python" => l.files.iter().any(|f| f.to_string_lossy().contains("__init__.py")),
1470                "JavaScript" | "TypeScript" => l.main_dependencies.is_empty(),
1471                _ => false,
1472            }
1473        });
1474        
1475        if has_lib_indicators {
1476            ProjectType::Library
1477        } else {
1478            ProjectType::Unknown
1479        }
1480    } else {
1481        ProjectType::Unknown
1482    }
1483}
1484
1485#[cfg(test)]
1486mod tests {
1487    use super::*;
1488    use crate::analyzer::{TechnologyCategory, LibraryType};
1489    use std::fs;
1490    use tempfile::TempDir;
1491    
1492    fn create_test_language(name: &str) -> DetectedLanguage {
1493        DetectedLanguage {
1494            name: name.to_string(),
1495            version: None,
1496            confidence: 0.9,
1497            files: vec![],
1498            main_dependencies: vec![],
1499            dev_dependencies: vec![],
1500            package_manager: None,
1501        }
1502    }
1503    
1504    fn create_test_technology(name: &str, category: TechnologyCategory) -> DetectedTechnology {
1505        DetectedTechnology {
1506            name: name.to_string(),
1507            version: None,
1508            category,
1509            confidence: 0.8,
1510            requires: vec![],
1511            conflicts_with: vec![],
1512            is_primary: false,
1513        }
1514    }
1515    
1516    #[test]
1517    fn test_node_project_context() {
1518        let temp_dir = TempDir::new().unwrap();
1519        let root = temp_dir.path();
1520        
1521        // Create package.json with scripts
1522        let package_json = r#"{
1523            "name": "test-app",
1524            "main": "index.js",
1525            "scripts": {
1526                "start": "node index.js",
1527                "dev": "nodemon index.js",
1528                "test": "jest",
1529                "build": "webpack"
1530            }
1531        }"#;
1532        fs::write(root.join("package.json"), package_json).unwrap();
1533        
1534        // Create index.js with port and env vars
1535        let index_js = r#"
1536const express = require('express');
1537const app = express();
1538
1539const PORT = process.env.PORT || 3000;
1540const API_KEY = process.env.API_KEY;
1541const DATABASE_URL = process.env.DATABASE_URL;
1542
1543app.listen(PORT, () => {
1544    console.log(`Server running on port ${PORT}`);
1545});
1546        "#;
1547        fs::write(root.join("index.js"), index_js).unwrap();
1548        
1549        let languages = vec![create_test_language("JavaScript")];
1550        let technologies = vec![create_test_technology("Express", TechnologyCategory::BackendFramework)];
1551        let config = AnalysisConfig::default();
1552        
1553        let context = analyze_context(root, &languages, &technologies, &config).unwrap();
1554        
1555        // Verify entry points
1556        assert!(!context.entry_points.is_empty());
1557        assert!(context.entry_points.iter().any(|ep| ep.file.ends_with("index.js")));
1558        
1559        // Verify ports
1560        assert!(!context.ports.is_empty());
1561        assert!(context.ports.iter().any(|p| p.number == 3000));
1562        
1563        // Verify environment variables
1564        assert!(context.environment_variables.iter().any(|ev| ev.name == "PORT"));
1565        assert!(context.environment_variables.iter().any(|ev| ev.name == "API_KEY"));
1566        assert!(context.environment_variables.iter().any(|ev| ev.name == "DATABASE_URL"));
1567        
1568        // Verify build scripts
1569        assert_eq!(context.build_scripts.len(), 4);
1570        assert!(context.build_scripts.iter().any(|bs| bs.name == "start" && bs.is_default));
1571        assert!(context.build_scripts.iter().any(|bs| bs.name == "dev" && bs.is_default));
1572        assert!(context.build_scripts.iter().any(|bs| bs.name == "test"));
1573        assert!(context.build_scripts.iter().any(|bs| bs.name == "build"));
1574        
1575        // Verify project type
1576        assert_eq!(context.project_type, ProjectType::WebApplication);
1577    }
1578    
1579    #[test]
1580    fn test_python_project_context() {
1581        let temp_dir = TempDir::new().unwrap();
1582        let root = temp_dir.path();
1583        
1584        // Create app.py with Flask
1585        let app_py = r#"
1586import os
1587from flask import Flask
1588
1589app = Flask(__name__)
1590
1591PORT = 5000
1592SECRET_KEY = os.environ.get('SECRET_KEY')
1593DEBUG = os.getenv('DEBUG', 'False')
1594
1595if __name__ == '__main__':
1596    app.run(port=PORT)
1597        "#;
1598        fs::write(root.join("app.py"), app_py).unwrap();
1599        
1600        let languages = vec![create_test_language("Python")];
1601        let technologies = vec![create_test_technology("Flask", TechnologyCategory::BackendFramework)];
1602        let config = AnalysisConfig::default();
1603        
1604        let context = analyze_context(root, &languages, &technologies, &config).unwrap();
1605        
1606        // Verify entry points
1607        assert!(context.entry_points.iter().any(|ep| ep.file.ends_with("app.py")));
1608        
1609        // Verify ports
1610        assert!(context.ports.iter().any(|p| p.number == 5000));
1611        
1612        // Verify environment variables
1613        assert!(context.environment_variables.iter().any(|ev| ev.name == "SECRET_KEY"));
1614        assert!(context.environment_variables.iter().any(|ev| ev.name == "DEBUG"));
1615        
1616        // Verify project type
1617        assert_eq!(context.project_type, ProjectType::WebApplication);
1618    }
1619    
1620    #[test]
1621    fn test_rust_project_context() {
1622        let temp_dir = TempDir::new().unwrap();
1623        let root = temp_dir.path();
1624        
1625        // Create Cargo.toml
1626        let cargo_toml = r#"
1627[package]
1628name = "test-server"
1629version = "0.1.0"
1630
1631[[bin]]
1632name = "server"
1633path = "src/main.rs"
1634        "#;
1635        fs::write(root.join("Cargo.toml"), cargo_toml).unwrap();
1636        
1637        // Create src directory
1638        fs::create_dir_all(root.join("src")).unwrap();
1639        
1640        // Create main.rs
1641        let main_rs = r#"
1642use std::env;
1643
1644fn main() {
1645    let port = env::var("PORT").unwrap_or_else(|_| "8080".to_string());
1646    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
1647    
1648    println!("Starting server on port {}", port);
1649}
1650        "#;
1651        fs::write(root.join("src/main.rs"), main_rs).unwrap();
1652        
1653        let languages = vec![create_test_language("Rust")];
1654        let frameworks = vec![];
1655        let config = AnalysisConfig::default();
1656        
1657        let context = analyze_context(root, &languages, &frameworks, &config).unwrap();
1658        
1659        // Verify entry points
1660        assert!(context.entry_points.iter().any(|ep| ep.file.ends_with("main.rs")));
1661        assert!(context.entry_points.iter().any(|ep| ep.command == Some("cargo run".to_string())));
1662        
1663        // Verify build scripts
1664        assert!(context.build_scripts.iter().any(|bs| bs.name == "build"));
1665        assert!(context.build_scripts.iter().any(|bs| bs.name == "test"));
1666        assert!(context.build_scripts.iter().any(|bs| bs.name == "run" && bs.is_default));
1667        
1668        // Verify environment variables
1669        assert!(context.environment_variables.iter().any(|ev| ev.name == "PORT"));
1670        assert!(context.environment_variables.iter().any(|ev| ev.name == "DATABASE_URL"));
1671    }
1672    
1673    #[test]
1674    fn test_dockerfile_analysis() {
1675        let temp_dir = TempDir::new().unwrap();
1676        let root = temp_dir.path();
1677        
1678        // Create Dockerfile
1679        let dockerfile = r#"
1680FROM node:14
1681WORKDIR /app
1682
1683ENV NODE_ENV=production
1684ENV PORT=3000
1685
1686EXPOSE 3000
1687EXPOSE 9229/tcp
1688
1689CMD ["node", "server.js"]
1690        "#;
1691        fs::write(root.join("Dockerfile"), dockerfile).unwrap();
1692        
1693        let languages = vec![];
1694        let frameworks = vec![];
1695        let config = AnalysisConfig::default();
1696        
1697        let context = analyze_context(root, &languages, &frameworks, &config).unwrap();
1698        
1699        // Verify ports from EXPOSE
1700        assert!(context.ports.iter().any(|p| p.number == 3000));
1701        assert!(context.ports.iter().any(|p| p.number == 9229 && p.protocol == Protocol::Tcp));
1702        
1703        // Verify environment variables from ENV
1704        assert!(context.environment_variables.iter().any(|ev| 
1705            ev.name == "NODE_ENV" && ev.default_value == Some("production".to_string())
1706        ));
1707        assert!(context.environment_variables.iter().any(|ev| 
1708            ev.name == "PORT" && ev.default_value == Some("3000".to_string())
1709        ));
1710    }
1711    
1712    #[test]
1713    fn test_docker_compose_analysis() {
1714        let temp_dir = TempDir::new().unwrap();
1715        let root = temp_dir.path();
1716        
1717        // Create docker-compose.yml
1718        let compose = r#"
1719version: '3.8'
1720services:
1721  web:
1722    build: .
1723    ports:
1724      - "8080:80"
1725      - "443"
1726    environment:
1727      - DATABASE_URL=postgres://user:pass@db:5432/mydb
1728      - REDIS_URL=redis://cache:6379
1729  db:
1730    image: postgres
1731    ports:
1732      - "5432"
1733    environment:
1734      POSTGRES_PASSWORD: secret
1735        "#;
1736        fs::write(root.join("docker-compose.yml"), compose).unwrap();
1737        
1738        let languages = vec![];
1739        let frameworks = vec![];
1740        let config = AnalysisConfig::default();
1741        
1742        let context = analyze_context(root, &languages, &frameworks, &config).unwrap();
1743        
1744        // Verify ports
1745        assert!(context.ports.iter().any(|p| p.number == 80));
1746        assert!(context.ports.iter().any(|p| p.number == 443));
1747        assert!(context.ports.iter().any(|p| p.number == 5432));
1748        
1749        // Verify environment variables
1750        assert!(context.environment_variables.iter().any(|ev| ev.name == "DATABASE_URL"));
1751        assert!(context.environment_variables.iter().any(|ev| ev.name == "REDIS_URL"));
1752        assert!(context.environment_variables.iter().any(|ev| ev.name == "POSTGRES_PASSWORD"));
1753    }
1754    
1755    #[test]
1756    fn test_env_file_analysis() {
1757        let temp_dir = TempDir::new().unwrap();
1758        let root = temp_dir.path();
1759        
1760        // Create .env file
1761        let env_file = r#"
1762# Database configuration
1763DATABASE_URL=postgresql://localhost:5432/myapp
1764REDIS_URL=redis://localhost:6379
1765
1766# API Keys
1767API_KEY=
1768SECRET_KEY=required
1769
1770# Feature flags
1771ENABLE_FEATURE_X=true
1772DEBUG=false
1773        "#;
1774        fs::write(root.join(".env"), env_file).unwrap();
1775        
1776        let languages = vec![];
1777        let frameworks = vec![];
1778        let config = AnalysisConfig::default();
1779        
1780        let context = analyze_context(root, &languages, &frameworks, &config).unwrap();
1781        
1782        // Verify environment variables
1783        assert!(context.environment_variables.iter().any(|ev| 
1784            ev.name == "DATABASE_URL" && ev.default_value.is_some()
1785        ));
1786        assert!(context.environment_variables.iter().any(|ev| 
1787            ev.name == "API_KEY" && ev.required
1788        ));
1789        assert!(context.environment_variables.iter().any(|ev| 
1790            ev.name == "SECRET_KEY" && ev.required
1791        ));
1792        assert!(context.environment_variables.iter().any(|ev| 
1793            ev.name == "ENABLE_FEATURE_X" && ev.default_value == Some("true".to_string())
1794        ));
1795    }
1796    
1797    #[test]
1798    fn test_makefile_analysis() {
1799        let temp_dir = TempDir::new().unwrap();
1800        let root = temp_dir.path();
1801        
1802        // Create Makefile
1803        let makefile = r#"
1804build:
1805	go build -o app main.go
1806
1807test:
1808	go test ./...
1809
1810run: build
1811	./app
1812
1813docker-build:
1814	docker build -t myapp .
1815
1816clean:
1817	rm -f app
1818        "#;
1819        fs::write(root.join("Makefile"), makefile).unwrap();
1820        
1821        let languages = vec![];
1822        let frameworks = vec![];
1823        let config = AnalysisConfig::default();
1824        
1825        let context = analyze_context(root, &languages, &frameworks, &config).unwrap();
1826        
1827        // Verify build scripts
1828        assert!(context.build_scripts.iter().any(|bs| bs.name == "build"));
1829        assert!(context.build_scripts.iter().any(|bs| bs.name == "test"));
1830        assert!(context.build_scripts.iter().any(|bs| bs.name == "run" && bs.is_default));
1831        assert!(context.build_scripts.iter().any(|bs| bs.name == "docker-build"));
1832        assert!(context.build_scripts.iter().any(|bs| bs.name == "clean"));
1833    }
1834    
1835    #[test]
1836    fn test_project_type_detection() {
1837        // Test CLI tool detection
1838        let languages = vec![create_test_language("Rust")];
1839        let technologies = vec![create_test_technology("clap", TechnologyCategory::Library(LibraryType::Other("CLI".to_string())))];
1840        let entry_points = vec![EntryPoint {
1841            file: PathBuf::from("src/main.rs"),
1842            function: Some("main".to_string()),
1843            command: Some("cargo run".to_string()),
1844        }];
1845        let ports = vec![];
1846        
1847        let project_type = determine_project_type(&languages, &technologies, &entry_points, &ports);
1848        assert_eq!(project_type, ProjectType::CliTool);
1849        
1850        // Test API service detection
1851        let technologies = vec![create_test_technology("FastAPI", TechnologyCategory::BackendFramework)];
1852        let ports = vec![Port {
1853            number: 8000,
1854            protocol: Protocol::Http,
1855            description: None,
1856        }];
1857        
1858        let project_type = determine_project_type(&languages, &technologies, &vec![], &ports);
1859        assert_eq!(project_type, ProjectType::ApiService);
1860        
1861        // Test library detection
1862        let languages = vec![create_test_language("Python")];
1863        let mut lang = languages[0].clone();
1864        lang.files = vec![PathBuf::from("__init__.py")];
1865        let languages = vec![lang];
1866        
1867        let project_type = determine_project_type(&languages, &vec![], &vec![], &vec![]);
1868        assert_eq!(project_type, ProjectType::Library);
1869    }
1870}