use std::path::Path;
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct Check {
pub ok: bool,
pub optional: bool,
pub message: String,
}
pub fn run(cfg: &crate::config::Config, root: &str) -> Vec<Check> {
let set = |k: &str| std::env::var(k).is_ok_and(|v| !v.is_empty());
let mut out = Vec::new();
let mut check = |good: bool, optional: bool, ok: &str, bad: &str| {
out.push(Check {
ok: good,
optional,
message: if good { ok } else { bad }.to_string(),
})
};
let file = std::fs::read_to_string(crate::config::config_path()).unwrap_or_default();
let obsolete: Vec<&str> = ["aws_profile", "sso_auto_login"]
.into_iter()
.filter(|k| {
file.lines().any(|l| {
l.trim_start()
.strip_prefix(k)
.is_some_and(|r| r.trim_start().starts_with('='))
})
})
.collect();
if !obsolete.is_empty() {
check(
false,
true,
"",
&format!(
"config.toml still sets {}, which comindly no longer reads: remove it and set AWS_PROFILE in the environment if you use a profile",
obsolete.join(" and ")
),
);
}
let graph = format!("{}/_graph", root.trim_end_matches('/'));
match crate::index::graph_versions_blocking(&graph) {
Ok(v) if !v.is_empty() => {
let (_, _, at) = v.last().cloned().unwrap_or_default();
let msg = format!(
"index readable: {} version(s), latest {}",
v.len(),
at.get(..16).unwrap_or(&at)
);
check(true, false, &msg, "");
}
_ => check(
false,
false,
"",
"no index here yet: run `comindly link <repos> --index-dir …`",
),
}
match crate::ledger::all_blocking(root) {
Ok(r) => check(
true,
false,
&format!("ledger readable: {} record(s)", r.len()),
"",
),
Err(e) => check(false, false, "", &format!("ledger not readable: {e:#}")),
}
if crate::cache::is_remote(root) {
let fresh = crate::cache::dir(root).is_some_and(|d| d.join("_graph").is_dir());
check(
fresh,
false,
"local mirror present (hooks read it)",
"no local mirror yet: `comindly cache refresh` (hooks create it on first use)",
);
}
let settings = std::env::var_os("HOME")
.map(|h| Path::new(&h).join(".claude/settings.json"))
.and_then(|p| std::fs::read_to_string(p).ok())
.unwrap_or_default();
let local = std::fs::read_to_string(".claude/settings.json").unwrap_or_default();
check(
settings.contains("comindly hook") || local.contains("comindly hook"),
true,
"Claude Code hooks configured",
"Claude Code hooks not found: `comindly init-hooks claude`",
);
check(
set("OPENAI_API_KEY")
|| cfg.llm_base_url().is_some()
|| crate::credentials::llm_key(crate::team::current().and_then(|t| t.name).as_deref())
.0
.is_some(),
true,
&format!("LLM configured (model {})", cfg.llm_model()),
"no LLM key: summaries, flows and style guides are off (indexing works without)",
);
check(
crate::signing::signing_key().is_some(),
true,
"ledger records are signed",
"records are unsigned (optional: `signing_key` or git SSH signing)",
);
check(
crate::signing::allowed_signers().is_some(),
true,
"signatures are verified against allowed_signers",
"no allowed_signers: signatures are not checked (optional)",
);
let indexers: Vec<&str> = [
"scip-python",
"scip-typescript",
"scip-go",
"rust-analyzer",
"scip-java",
]
.into_iter()
.filter(|b| crate::scip::which(b).is_some())
.collect();
check(
!indexers.is_empty(),
true,
&format!("SCIP indexers on PATH: {}", indexers.join(", ")),
"no SCIP indexers on PATH (optional: compiler-grade edges)",
);
out
}
pub fn agents() -> Vec<&'static str> {
let Some(home) = std::env::var_os("HOME").map(std::path::PathBuf::from) else {
return Vec::new();
};
let json = |p: &str| -> serde_json::Value {
std::fs::read_to_string(home.join(p))
.ok()
.and_then(|t| serde_json::from_str(&t).ok())
.unwrap_or_default()
};
let has = |v: &serde_json::Value| {
v.get("mcpServers")
.and_then(|m| m.get("comindly"))
.is_some()
};
let claude = json(".claude.json");
let mut out = Vec::new();
if has(&claude)
|| claude["projects"]
.as_object()
.is_some_and(|p| p.values().any(has))
{
out.push("claude");
}
if has(&json(".cursor/mcp.json")) {
out.push("cursor");
}
if std::fs::read_to_string(home.join(".codex/config.toml"))
.is_ok_and(|t| t.contains("[mcp_servers.comindly]"))
{
out.push("codex");
}
out
}