robin/tools/
required_tools.rs

1use std::process::Command;
2use anyhow::{Result, Context};
3use crate::config::RobinConfig;
4
5#[derive(Debug, PartialEq)]
6pub struct RequiredTool {
7    pub name: &'static str,
8    pub command: &'static str,
9    pub version_arg: &'static str,
10    pub patterns: &'static [&'static str],
11}
12
13pub const KNOWN_TOOLS: &[RequiredTool] = &[
14    RequiredTool {
15        name: "Node.js",
16        command: "node",
17        version_arg: "--version",
18        patterns: &["node ", "npm ", "npx "],
19    },
20    RequiredTool {
21        name: "Python",
22        command: "python",
23        version_arg: "--version",
24        patterns: &["python ", "pip ", "python3 "],
25    },
26    RequiredTool {
27        name: "Ruby",
28        command: "ruby",
29        version_arg: "--version",
30        patterns: &["ruby ", "gem ", "bundle "],
31    },
32    RequiredTool {
33        name: "Fastlane",
34        command: "fastlane",
35        version_arg: "--version",
36        patterns: &["fastlane "],
37    },
38    RequiredTool {
39        name: "Flutter",
40        command: "flutter",
41        version_arg: "--version",
42        patterns: &["flutter "],
43    },
44    RequiredTool {
45        name: "Cargo",
46        command: "cargo",
47        version_arg: "--version",
48        patterns: &["cargo "],
49    },
50    RequiredTool {
51        name: "Go",
52        command: "go",
53        version_arg: "version",
54        patterns: &["go "],
55    },
56    RequiredTool {
57        name: "ADB",
58        command: "adb",
59        version_arg: "version",
60        patterns: &["adb "],
61    },
62    RequiredTool {
63        name: "Gradle",
64        command: "gradle",
65        version_arg: "--version",
66        patterns: &["gradle ", "./gradlew "],
67    },
68    RequiredTool {
69        name: "CocoaPods",
70        command: "pod",
71        version_arg: "--version",
72        patterns: &["pod ", "cocoapods "],
73    },
74    RequiredTool {
75        name: "Xcode CLI",
76        command: "xcrun",
77        version_arg: "--version",
78        patterns: &["xcrun ", "xcodebuild "],
79    },
80    RequiredTool {
81        name: "Docker",
82        command: "docker",
83        version_arg: "--version",
84        patterns: &["docker "],
85    },
86    RequiredTool {
87        name: "Git",
88        command: "git",
89        version_arg: "--version",
90        patterns: &["git "],
91    },
92    RequiredTool {
93        name: "Maven",
94        command: "mvn",
95        version_arg: "--version",
96        patterns: &["mvn ", "maven "],
97    },
98];
99
100fn check_script_contains(script: &serde_json::Value, pattern: &str) -> bool {
101    match script {
102        serde_json::Value::String(cmd) => cmd.contains(pattern),
103        serde_json::Value::Array(commands) => {
104            commands.iter().any(|cmd| {
105                cmd.as_str().map_or(false, |s| s.contains(pattern))
106            })
107        },
108        _ => false,
109    }
110}
111
112fn detect_required_tools(config: &RobinConfig) -> Vec<&'static RequiredTool> {
113    KNOWN_TOOLS
114        .iter()
115        .filter(|tool| {
116            config.scripts.values().any(|script| {
117                tool.patterns.iter().any(|&pattern| check_script_contains(script, pattern))
118            })
119        })
120        .collect()
121}
122
123pub fn check_environment() -> Result<(bool, usize, usize, std::time::Duration)> {
124    let start_time = std::time::Instant::now();
125    let mut all_checks_passed = true;
126    let mut found_tools = 0;
127    let mut missing_tools = 0;
128
129    let config_path = std::path::PathBuf::from(crate::CONFIG_FILE);
130    let config = RobinConfig::load(&config_path)
131        .with_context(|| "No .robin.json found. Run 'robin init' first")?;
132
133    println!("šŸ” Checking development environment...\n");
134
135    // Check Required Tools
136    let required_tools = detect_required_tools(&config);
137    if !required_tools.is_empty() {
138        println!("šŸ“¦ Required Tools:");
139        for tool in &required_tools {
140            match Command::new(tool.command).arg(tool.version_arg).output() {
141                Ok(output) if output.status.success() => {
142                    found_tools += 1;
143                    let stdout = String::from_utf8_lossy(&output.stdout);
144                    let version = stdout.lines().next().unwrap_or("").trim();
145                    println!("āœ… {}: {}", tool.name, version);
146                }
147                _ => {
148                    missing_tools += 1;
149                    all_checks_passed = false;
150                    println!("āŒ {} not found", tool.name);
151                }
152            }
153        }
154    }
155
156    // Check Environment Variables if needed tools are detected
157    let needs_android = required_tools.iter().any(|t| t.name == "Flutter");
158    let needs_java = needs_android || config.scripts.values().any(|s| 
159        check_script_contains(s, "java ") || check_script_contains(s, "gradle ")
160    );
161    
162    if needs_android || needs_java {
163        println!("\nšŸ”§ Environment Variables:");
164        if needs_android {
165            if std::env::var("ANDROID_HOME").is_ok() {
166                found_tools += 1;
167                println!("āœ… ANDROID_HOME is set");
168            } else {
169                missing_tools += 1;
170                all_checks_passed = false;
171                println!("āŒ ANDROID_HOME is not set");
172            }
173        }
174        if needs_java {
175            if std::env::var("JAVA_HOME").is_ok() {
176                found_tools += 1;
177                println!("āœ… JAVA_HOME is set");
178            } else {
179                missing_tools += 1;
180                all_checks_passed = false;
181                println!("āŒ JAVA_HOME is not set");
182            }
183        }
184    }
185
186    // Check Git Configuration if git commands are used
187    if config.scripts.values().any(|s| check_script_contains(s, "git ")) {
188        println!("\nšŸ” Git Configuration:");
189        for key in ["user.name", "user.email"].iter() {
190            match Command::new("git").args(["config", key]).output() {
191                Ok(output) if output.status.success() => {
192                    found_tools += 1;
193                    println!("āœ… Git {} is set", key);
194                }
195                _ => {
196                    missing_tools += 1;
197                    all_checks_passed = false;
198                    println!("āŒ Git {} is not set", key);
199                }
200            }
201        }
202    }
203
204    let duration = start_time.elapsed();
205    Ok((all_checks_passed, found_tools, missing_tools, duration))
206}
207
208pub fn update_tools() -> Result<(bool, Vec<String>)> {
209    let config_path = std::path::PathBuf::from(crate::CONFIG_FILE);
210    let config = RobinConfig::load(&config_path)
211        .with_context(|| "No .robin.json found. Run 'robin init' first")?;
212
213    let required_tools = detect_required_tools(&config);
214    let mut updated_tools = Vec::new();
215    let mut all_success = true;
216
217    println!("šŸ”„ Updating development tools...\n");
218
219    for tool in required_tools {
220        match tool.name {
221            "Node.js" => {
222                if Command::new("npm").arg("--version").output().is_ok() {
223                    println!("Updating npm packages...");
224                    if !run_update_command("npm", &["update", "-g"])? {
225                        all_success = false;
226                    } else {
227                        updated_tools.push("npm packages".to_string());
228                    }
229                }
230            },
231            "Ruby" | "Fastlane" => {
232                if Command::new("gem").arg("--version").output().is_ok() {
233                    println!("Updating Fastlane...");
234                    if !run_update_command("gem", &["update", "fastlane"])? {
235                        all_success = false;
236                    } else {
237                        updated_tools.push("Fastlane".to_string());
238                    }
239                }
240            },
241            "Flutter" => {
242                if Command::new("flutter").arg("--version").output().is_ok() {
243                    println!("Updating Flutter...");
244                    if !run_update_command("flutter", &["upgrade"])? {
245                        all_success = false;
246                    } else {
247                        updated_tools.push("Flutter".to_string());
248                    }
249                }
250            },
251            "Cargo" => {
252                if Command::new("rustup").arg("--version").output().is_ok() {
253                    println!("Updating Rust toolchain...");
254                    if !run_update_command("rustup", &["update"])? {
255                        all_success = false;
256                    } else {
257                        updated_tools.push("Rust".to_string());
258                    }
259                }
260            },
261            "CocoaPods" => {
262                if cfg!(target_os = "macos") && Command::new("pod").arg("--version").output().is_ok() {
263                    println!("Updating CocoaPods repos...");
264                    if !run_update_command("pod", &["repo", "update"])? {
265                        all_success = false;
266                    } else {
267                        updated_tools.push("CocoaPods".to_string());
268                    }
269                }
270            },
271            _ => {}
272        }
273    }
274
275    if updated_tools.is_empty() {
276        println!("No tools to update!");
277    } else {
278        println!("\nāœ… Update complete!");
279    }
280
281    Ok((all_success, updated_tools))
282}
283
284fn run_update_command(cmd: &str, args: &[&str]) -> Result<bool> {
285    let status = Command::new(cmd)
286        .args(args)
287        .status()
288        .with_context(|| format!("Failed to run {} update", cmd))?;
289
290    if !status.success() {
291        println!("āŒ {} update failed", cmd);
292    }
293    Ok(status.success())
294}