comindly_engine/
doctor.rs1use std::path::Path;
4
5pub use comindly_api::doctor::Check;
6
7pub fn run(
9 team: &crate::team::TeamSettings,
10 server: Option<&crate::server::Server>,
11 root: &str,
12) -> Vec<Check> {
13 let resolved = crate::context::Settings::resolve(team, crate::config::env);
14 let set = |k: &str| std::env::var(k).is_ok_and(|v| !v.is_empty());
15 let mut out = Vec::new();
16 let mut check = |good: bool, optional: bool, ok: &str, bad: &str| {
17 out.push(Check {
18 ok: good,
19 optional,
20 message: if good { ok } else { bad }.to_string(),
21 })
22 };
23 let graph = format!("{}/_graph", root.trim_end_matches('/'));
24 match crate::index::graph_versions_blocking(&graph) {
25 Ok(v) if !v.is_empty() => {
26 let (_, _, at) = v.last().cloned().unwrap_or_default();
27 let latest = chrono::DateTime::parse_from_rfc3339(&at)
28 .map(|t| t.to_utc().format("%-d %b %Y, %H:%M UTC").to_string())
29 .unwrap_or(at);
30 let msg = format!(
31 "index readable: {}, latest {latest}",
32 plural(v.len(), "version")
33 );
34 check(true, false, &msg, "");
35 }
36 _ => check(
37 false,
38 false,
39 "",
40 "no index here yet: run `comindly link <repos> --index-dir …`",
41 ),
42 }
43 match crate::ledger::all_blocking(root) {
44 Ok(r) => check(
45 true,
46 false,
47 &format!("ledger readable: {}", plural(r.len(), "record")),
48 "",
49 ),
50 Err(e) => check(false, false, "", &format!("ledger not readable: {e:#}")),
51 }
52 if crate::cache::is_remote(root) {
53 let fresh = crate::cache::dir(root).is_some_and(|d| d.join("_graph").is_dir());
54 check(
55 fresh,
56 false,
57 "local mirror present (hooks read it)",
58 "no local mirror yet: `comindly cache refresh` (hooks create it on first use)",
59 );
60 }
61 let settings = std::env::var_os("HOME")
62 .map(|h| Path::new(&h).join(".claude/settings.json"))
63 .and_then(|p| std::fs::read_to_string(p).ok())
64 .unwrap_or_default();
65 let local = std::fs::read_to_string(".claude/settings.json").unwrap_or_default();
66 check(
67 settings.contains("comindly hook") || local.contains("comindly hook"),
68 true,
69 "Claude Code hooks configured",
70 "Claude Code hooks not found: `comindly init-hooks claude`",
71 );
72 check(
73 set("OPENAI_API_KEY")
74 || resolved.llm_base_url.is_some()
75 || crate::credentials::llm_key(team.name.as_deref(), server)
76 .0
77 .is_some(),
78 true,
79 &format!("LLM configured (model {})", resolved.llm_model),
80 "no LLM key: summaries, flows and style guides are off (indexing works without)",
81 );
82 check(
83 crate::config::local_signing_key().is_some(),
84 true,
85 "ledger records are signed",
86 "records are unsigned (optional: `signing_key` or git SSH signing)",
87 );
88 check(
89 crate::config::local_allowed_signers().is_some(),
90 true,
91 "signatures are verified against `allowed_signers`",
92 "no `allowed_signers`: signatures are not checked (optional)",
93 );
94 let indexers: Vec<&str> = [
95 "scip-python",
96 "scip-typescript",
97 "scip-go",
98 "rust-analyzer",
99 "scip-java",
100 ]
101 .into_iter()
102 .filter(|b| crate::scip::which(b).is_some())
103 .collect();
104 check(
105 !indexers.is_empty(),
106 true,
107 &format!("SCIP indexers on PATH: {}", indexers.join(", ")),
108 "no SCIP indexers on PATH (optional: compiler-grade edges)",
109 );
110 out
111}
112
113pub use comindly_api::agents::agents;
114
115pub(crate) fn plural(n: usize, noun: &str) -> String {
117 format!("{n} {noun}{}", if n == 1 { "" } else { "s" })
118}