crates/signaltty-cli/src/integration.rs

//! Reporting adapter for the shared hook installer (ADR-0013).
use crate::client::CliError;
use signaltty_integration::{Hooks, Report};
use std::path::{Path, PathBuf};

pub fn home_dir(override_home: Option<&str>) -> Result<PathBuf, CliError> {
    override_home
        .map(PathBuf::from)
        .or_else(|| std::env::var_os("HOME").map(PathBuf::from))
        .ok_or_else(|| CliError::Usage("HOME is not set (use --home)".into()))
}
pub fn valid_agents() -> &'static [&'static str] {
    signaltty_integration::AGENTS
}
fn hooks(home: Option<&Path>) -> Result<Hooks, CliError> {
    Hooks::from_env(
        home,
        std::env::current_exe().map_err(|e| CliError::Io(e.to_string()))?,
    )
    .map_err(|e| CliError::Usage(e.to_string()))
}
fn print(report: Report, removing: bool, json: bool) {
    if json {
        println!("{}", serde_json::to_value(report).unwrap());
    } else {
        println!(
            "{} {} hooks ({})",
            if removing {
                "removed"
            } else if report.changed {
                "configured"
            } else {
                "already configured"
            },
            report.agent,
            report.file.display()
        );
        if let Some(notice) = report.notice {
            eprintln!("{notice}");
        }
    }
}
pub fn install(home: Option<&Path>, agent: &str, json: bool) -> Result<(), CliError> {
    print(
        hooks(home)?
            .install(agent)
            .map_err(|e| CliError::Usage(e.to_string()))?,
        false,
        json,
    );
    Ok(())
}
pub fn uninstall(home: Option<&Path>, agent: &str, json: bool) -> Result<(), CliError> {
    print(
        hooks(home)?
            .uninstall(agent)
            .map_err(|e| CliError::Usage(e.to_string()))?,
        true,
        json,
    );
    Ok(())
}
pub fn status(home: Option<&Path>, agents: &[String], json: bool) -> Result<(), CliError> {
    let list: Vec<&str> = if agents.is_empty() {
        valid_agents().to_vec()
    } else {
        agents.iter().map(String::as_str).collect()
    };
    let hooks = hooks(home)?;
    let mut out = serde_json::json!({});
    for agent in &list {
        let path = hooks
            .file_for(agent)
            .map_err(|e| CliError::Usage(e.to_string()))?;
        let installed = hooks
            .installed(agent)
            .map_err(|e| CliError::Usage(e.to_string()))?;
        out[agent] = serde_json::json!({"installed": installed, "file": path,
            "trust": if *agent == "codex" { "provider_review" } else { "provider_managed" }});
    }
    // Detection overlays (data, not code): manifests in the agents dir.
    let agents_dir = std::env::var("SIGNALTTY_AGENTS_DIR")
        .map(std::path::PathBuf::from)
        .unwrap_or_else(|_| signaltty_core::paths::agents_dir());
    let mut manifests = Vec::new();
    if let Ok(entries) = std::fs::read_dir(&agents_dir) {
        let mut files: Vec<_> = entries
            .flatten()
            .map(|e| e.path())
            .filter(|p| p.extension().is_some_and(|e| e == "toml"))
            .collect();
        files.sort();
        for path in files {
            let name = path
                .file_stem()
                .map(|s| s.to_string_lossy().to_string())
                .unwrap_or_default();
            let entry = match std::fs::read_to_string(&path) {
                Ok(text) => match signaltty_agent::parse_manifest(&text) {
                    Ok(m) => serde_json::json!({
                        "file": path.display().to_string(),
                        "kind": m.kind().map(|k| k.as_str().to_string()).unwrap_or_default(),
                        "ok": true,
                    }),
                    Err(e) => serde_json::json!({
                        "file": path.display().to_string(),
                        "ok": false, "error": e,
                    }),
                },
                Err(e) => serde_json::json!({
                    "file": path.display().to_string(),
                    "ok": false, "error": e.to_string(),
                }),
            };
            manifests.push(serde_json::json!({"name": name, "manifest": entry}));
        }
    }
    out["manifests"] = serde_json::Value::Array(manifests);
    if json {
        println!("{out}");
    } else {
        for agent in &list {
            let e = &out[agent];
            println!(
                "{agent}: {} ({})",
                if e["installed"].as_bool().unwrap() {
                    "installed"
                } else {
                    "not installed"
                },
                e["file"].as_str().unwrap()
            );
        }
        let manifests = out["manifests"].as_array().cloned().unwrap_or_default();
        if manifests.is_empty() {
            println!("manifests: none ({})", agents_dir.display());
        }
        for m in &manifests {
            let inner = &m["manifest"];
            if inner["ok"].as_bool().unwrap_or(false) {
                println!(
                    "manifest {}: kind={} ({})",
                    m["name"].as_str().unwrap_or("?"),
                    inner["kind"].as_str().unwrap_or("?"),
                    inner["file"].as_str().unwrap_or("?"),
                );
            } else {
                println!(
                    "manifest {}: BROKEN: {} ({})",
                    m["name"].as_str().unwrap_or("?"),
                    inner["error"].as_str().unwrap_or("?"),
                    inner["file"].as_str().unwrap_or("?"),
                );
            }
        }
    }
    Ok(())
}