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 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 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 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}