opencode_ralph_loop_cli/commands/
doctor.rs1use std::path::Path;
2use std::process::Command;
3
4use crate::cli::OutputFormat;
5use crate::error::CliError;
6
7struct DepStatus {
8 name: String,
9 available: bool,
10 version: Option<String>,
11}
12
13pub fn run(path: &Path, output: &OutputFormat) -> Result<(), CliError> {
14 let deps = vec![check_dep("bun"), check_dep("node"), check_dep("opencode")];
15
16 let write_ok = path
17 .metadata()
18 .map(|m| !m.permissions().readonly())
19 .unwrap_or(false);
20
21 let node_modules_ok = path.join(".opencode").join("node_modules").exists();
22
23 let all_ok = deps.iter().all(|d| d.available) && write_ok;
24
25 match output {
26 OutputFormat::Json => {
27 let obj: Vec<_> = deps
28 .iter()
29 .map(|d| {
30 serde_json::json!({
31 "name": d.name,
32 "available": d.available,
33 "version": d.version,
34 })
35 })
36 .collect();
37 let json = serde_json::to_string_pretty(&serde_json::json!({
38 "status": if all_ok { "ok" } else { "degraded" },
39 "dependencies": obj,
40 "write_permission": write_ok,
41 "node_modules_installed": node_modules_ok,
42 }))
43 .unwrap_or_default();
44 println!("{json}");
45 }
46 _ => {
47 for dep in &deps {
48 let status = if dep.available { "OK " } else { "MISSING" };
49 let ver = dep.version.as_deref().unwrap_or("not detected");
50 println!("{status} {}: {ver}", dep.name);
51 }
52 let wp_status = if write_ok { "OK " } else { "MISSING" };
53 println!("{wp_status} write_permission: {}", path.display());
54 let nm_status = if node_modules_ok {
55 "OK "
56 } else {
57 "MISSING"
58 };
59 println!("{nm_status} node_modules: run bun install or npm install");
60 println!("\nStatus: {}", if all_ok { "ok" } else { "degraded" });
61 }
62 }
63
64 if !all_ok {
65 return Err(CliError::Generic(
66 "missing dependencies detected".to_string(),
67 ));
68 }
69
70 Ok(())
71}
72
73fn check_dep(name: &str) -> DepStatus {
74 match Command::new(name).arg("--version").output() {
75 Ok(out) if out.status.success() => {
76 let version = String::from_utf8_lossy(&out.stdout)
77 .lines()
78 .next()
79 .map(str::trim)
80 .map(String::from);
81 DepStatus {
82 name: name.to_string(),
83 available: true,
84 version,
85 }
86 }
87 _ => DepStatus {
88 name: name.to_string(),
89 available: false,
90 version: None,
91 },
92 }
93}