Skip to main content

vtcode_core/commands/
analyze.rs

1//! Analyze command implementation - workspace analysis
2
3use crate::config::constants::tools;
4use crate::config::types::{AgentConfig, AnalysisDepth, OutputFormat};
5use crate::tools::ToolRegistry;
6use crate::utils::colors::style;
7use anyhow::Result;
8use serde_json::json;
9
10/// Handle the analyze command - comprehensive workspace analysis
11pub async fn handle_analyze_command(config: AgentConfig, depth: String, format: String) -> Result<()> {
12    println!("{}", style("Analyzing workspace...").cyan().bold());
13
14    let depth = match depth.to_lowercase().as_str() {
15        "basic" => AnalysisDepth::Basic,
16        "standard" => AnalysisDepth::Standard,
17        "deep" => AnalysisDepth::Deep,
18        _ => {
19            println!("{}", style("Invalid depth. Using 'standard'.").red());
20            AnalysisDepth::Standard
21        }
22    };
23
24    let _output_format = match format.to_lowercase().as_str() {
25        "text" => OutputFormat::Text,
26        "json" => OutputFormat::Json,
27        "html" => OutputFormat::Html,
28        _ => {
29            println!("{}", style("Invalid format. Using 'text'.").red());
30            OutputFormat::Text
31        }
32    };
33
34    let registry = ToolRegistry::new(config.workspace.clone()).await;
35    registry.mark_tool_preapproved(tools::LIST_FILES).await;
36
37    // Step 1: Get high-level directory structure
38    println!("{}", style("1. Getting workspace structure...").dim());
39    let root_files = registry
40        .execute_tool(
41            tools::LIST_FILES,
42            json!({"mode": "list", "path": ".", "per_page": 50, "max_items": 200, "include_hidden": false}),
43        )
44        .await;
45
46    let entry_names = match root_files {
47        Ok(result) => {
48            println!("{}", style("Root directory structure obtained").green());
49            let names = extract_entry_names(&result);
50            println!("   Found {} files/directories in root", names.len());
51            names
52        }
53        Err(e) => {
54            println!("{} {}", style("Failed to list root directory:").red(), e);
55            Vec::new()
56        }
57    };
58
59    // Step 2: Look for important project files
60    println!("{}", style("2. Identifying project type...").dim());
61    let important_files = vec![
62        "README.md",
63        "Cargo.toml",
64        "package.json",
65        "go.mod",
66        "requirements.txt",
67        "Makefile",
68    ];
69
70    for file in important_files {
71        if entry_names.iter().any(|name| name == file) {
72            println!("   {} {}", style("Detected").green(), file);
73        }
74    }
75
76    // Step 3: Read key configuration files
77    println!("{}", style("3. Reading project configuration...").dim());
78    let config_files = vec!["AGENTS.md", "README.md", "Cargo.toml", "package.json"];
79
80    for config_file in config_files {
81        let config_path = config.workspace.join(config_file);
82        let Ok(metadata) = tokio::fs::metadata(&config_path).await else {
83            continue;
84        };
85        println!("   {} {} ({} bytes)", style("Read").green(), config_file, metadata.len());
86    }
87
88    // Step 4: Analyze source code structure
89    println!("{}", style("4. Analyzing source code structure...").dim());
90
91    // Check for common source directories
92    let src_dirs = vec!["src", "lib", "pkg", "internal", "cmd"];
93    for dir in src_dirs {
94        if entry_names.iter().any(|name| name == dir) {
95            println!("   {} source directory: {}", style("Found").green(), dir);
96        }
97    }
98
99    if matches!(depth, AnalysisDepth::Deep) {
100        println!("{}", style("Deep analysis: use grep/search tools for detailed code inspection.").dim());
101    }
102
103    println!("{}", style("Workspace analysis complete!").green().bold());
104    println!("{}", style("You can now ask me specific questions about the codebase.").dim());
105
106    Ok(())
107}
108
109/// Extract entry names from a `list_files` tool result payload.
110///
111/// The list tool emits its entries under the `items` field; each entry carries
112/// a `name` (and a `path`). Returns an empty vector for missing or malformed
113/// payloads.
114fn extract_entry_names(result: &serde_json::Value) -> Vec<String> {
115    result
116        .get("items")
117        .and_then(|items| items.as_array())
118        .map(|entries| {
119            entries
120                .iter()
121                .filter_map(|entry| entry.get("name").and_then(|name| name.as_str()))
122                .map(|name| name.to_string())
123                .collect()
124        })
125        .unwrap_or_default()
126}
127
128#[cfg(test)]
129mod tests {
130    use super::extract_entry_names;
131    use serde_json::json;
132
133    #[test]
134    fn extracts_names_from_items_payload() {
135        let result = json!({
136            "success": true,
137            "items": [
138                {"name": "src", "path": "src", "type": "directory"},
139                {"name": "README.md", "path": "README.md", "type": "file"}
140            ]
141        });
142        assert_eq!(extract_entry_names(&result), vec!["src", "README.md"]);
143    }
144
145    #[test]
146    fn returns_empty_for_empty_items() {
147        let result = json!({"success": true, "items": []});
148        assert!(extract_entry_names(&result).is_empty());
149    }
150
151    #[test]
152    fn returns_empty_when_items_field_missing() {
153        let result = json!({"success": true, "count": 0});
154        assert!(extract_entry_names(&result).is_empty());
155    }
156}