Skip to main content

opencode_ralph_loop_cli/commands/
doctor.rs

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