opencode-ralph-loop-cli 0.1.0

Scaffolder CLI for OpenCode Ralph Loop plugin — one command setup
Documentation
use std::path::Path;
use std::process::Command;

use crate::cli::OutputFormat;
use crate::error::CliError;

struct DepStatus {
    name: String,
    available: bool,
    version: Option<String>,
}

pub fn run(path: &Path, output: &OutputFormat) -> Result<(), CliError> {
    let deps = vec![check_dep("bun"), check_dep("node"), check_dep("opencode")];

    let write_ok = path
        .metadata()
        .map(|m| !m.permissions().readonly())
        .unwrap_or(false);

    let node_modules_ok = path.join(".opencode").join("node_modules").exists();

    let all_ok = deps.iter().all(|d| d.available) && write_ok;

    match output {
        OutputFormat::Json => {
            let obj: Vec<_> = deps
                .iter()
                .map(|d| {
                    serde_json::json!({
                        "name": d.name,
                        "available": d.available,
                        "version": d.version,
                    })
                })
                .collect();
            let json = serde_json::to_string_pretty(&serde_json::json!({
                "status": if all_ok { "ok" } else { "degraded" },
                "dependencies": obj,
                "write_permission": write_ok,
                "node_modules_installed": node_modules_ok,
            }))
            .unwrap_or_default();
            println!("{json}");
        }
        _ => {
            for dep in &deps {
                let status = if dep.available { "OK     " } else { "MISSING" };
                let ver = dep.version.as_deref().unwrap_or("not detected");
                println!("{status} {}: {ver}", dep.name);
            }
            let wp_status = if write_ok { "OK     " } else { "MISSING" };
            println!("{wp_status} write_permission: {}", path.display());
            let nm_status = if node_modules_ok {
                "OK     "
            } else {
                "MISSING"
            };
            println!("{nm_status} node_modules: run bun install or npm install");
            println!("\nStatus: {}", if all_ok { "ok" } else { "degraded" });
        }
    }

    if !all_ok {
        return Err(CliError::Generic(
            "missing dependencies detected".to_string(),
        ));
    }

    Ok(())
}

fn check_dep(name: &str) -> DepStatus {
    match Command::new(name).arg("--version").output() {
        Ok(out) if out.status.success() => {
            let version = String::from_utf8_lossy(&out.stdout)
                .lines()
                .next()
                .map(str::trim)
                .map(String::from);
            DepStatus {
                name: name.to_string(),
                available: true,
                version,
            }
        }
        _ => DepStatus {
            name: name.to_string(),
            available: false,
            version: None,
        },
    }
}