1use crate::core::config;
5use crate::shell::cli::workspace_path;
6use crate::shell::init;
7use crate::store::Store;
8use anyhow::Result;
9use std::fmt::Write;
10use std::io::IsTerminal;
11use std::path::Path;
12
13pub fn doctor_text(workspace: Option<&Path>) -> Result<(i32, String)> {
15 let ws = workspace_path(workspace)?;
16 let mut hard_fail = false;
17 let mut out = String::new();
18
19 writeln!(&mut out, "kaizen {} (doctor)", env!("CARGO_PKG_VERSION")).unwrap();
20 writeln!(&mut out, "workspace: {}", ws.display()).unwrap();
21 writeln!(&mut out).unwrap();
22
23 let data_dir = crate::core::paths::project_data_dir(&ws).ok();
24 let wcfg_ex = data_dir
25 .as_ref()
26 .is_some_and(|d| d.join("config.toml").exists());
27 writeln!(
28 &mut out,
29 "project config.toml: {}",
30 if wcfg_ex { "present" } else { "absent" }
31 )
32 .unwrap();
33 match crate::core::paths::kaizen_dir() {
34 Some(kd) => writeln!(
35 &mut out,
36 "~/.kaizen/config.toml: {}",
37 if kd.join("config.toml").exists() {
38 "present"
39 } else {
40 "absent"
41 }
42 )
43 .unwrap(),
44 None => writeln!(
45 &mut out,
46 "~/.kaizen/config.toml: (KAIZEN_HOME / HOME unset, skipped)"
47 )
48 .unwrap(),
49 }
50 match crate::core::machine_registry::status() {
51 Ok(None) => writeln!(
52 &mut out,
53 "machine registry: (KAIZEN_HOME / HOME unset, skipped)"
54 )
55 .unwrap(),
56 Ok(Some((ref path, n))) => writeln!(
57 &mut out,
58 "machine registry: OK ({}; {} project(s))",
59 path.display(),
60 n
61 )
62 .unwrap(),
63 Err(e) => {
64 hard_fail = true;
65 writeln!(&mut out, "machine registry: ERROR: {e}").unwrap();
66 }
67 }
68 writeln!(&mut out).unwrap();
69
70 let cfg = match config::load(&ws) {
71 Ok(c) => c,
72 Err(e) => {
73 writeln!(&mut out, "config load: ERROR: {e}").unwrap();
74 return Ok((1, out));
75 }
76 };
77
78 writeln!(&mut out, "config (merged, no secrets):").unwrap();
79 writeln!(&mut out, " scan.roots: {} entries", cfg.scan.roots.len()).unwrap();
80 for (i, r) in cfg.scan.roots.iter().take(3).enumerate() {
81 let exp = crate::shell::cli::expand_home(r);
82 let exists = Path::new(&exp).exists();
83 writeln!(&mut out, " [{}] {} → exists={}", i + 1, r, exists).unwrap();
84 }
85 if cfg.scan.roots.len() > 3 {
86 writeln!(&mut out, " …").unwrap();
87 }
88 writeln!(
89 &mut out,
90 " sources.cursor: enabled={} glob={}",
91 cfg.sources.cursor.enabled, cfg.sources.cursor.transcript_glob
92 )
93 .unwrap();
94 let t = &cfg.sources.tail;
95 writeln!(
96 &mut out,
97 " sources.tail: goose={} opencode={} copilot_cli={} copilot_vscode={}",
98 t.goose, t.opencode, t.copilot_cli, t.copilot_vscode
99 )
100 .unwrap();
101 let sync_on = !cfg.sync.endpoint.is_empty() && !cfg.sync.team_id.is_empty();
102 writeln!(&mut out, " sync: endpoint configured: {}", sync_on).unwrap();
103 writeln!(&mut out).unwrap();
104
105 let db_result = crate::core::workspace::db_path(&ws);
106 let ws_key = ws.to_string_lossy().to_string();
107 match db_result.and_then(|db| Store::open(&db).map(|s| (db, s))) {
108 Ok((db, store)) => {
109 writeln!(&mut out, "store: OK ({})", db.display()).unwrap();
110 if let Ok(sessions) = store.list_sessions(&ws_key) {
111 writeln!(
112 &mut out,
113 " sessions in store (this workspace key): {}",
114 sessions.len()
115 )
116 .unwrap();
117 }
118 if let Ok(data_dir) = crate::core::paths::project_data_dir(&ws)
119 && let Ok(query) = crate::store::query::QueryStore::open(&data_dir)
120 && let Ok(stats) = query.summary_stats(&store, &ws_key)
121 && crate::shell::cli::summary_needs_cost_rollup_note(
122 stats.session_count,
123 stats.total_cost_usd_e6,
124 )
125 {
126 writeln!(
127 &mut out,
128 " {}",
129 crate::shell::cli::cost_rollup_zero_doctor_hint()
130 )
131 .unwrap();
132 }
133 let probe = db.parent().map(|p| p.join(".kaizen_write_probe"));
134 if let Some(probe) = probe {
135 let ok = std::fs::File::create(&probe).is_ok();
136 if ok {
137 let _ = std::fs::remove_file(&probe);
138 }
139 writeln!(
140 &mut out,
141 "project data dir writable: {}",
142 if ok { "yes" } else { "no" }
143 )
144 .unwrap();
145 if !ok {
146 hard_fail = true;
147 }
148 }
149 }
150 Err(e) => {
151 hard_fail = true;
152 writeln!(&mut out, "store: ERROR: {e}").unwrap();
153 }
154 }
155 writeln!(&mut out).unwrap();
156
157 let cursor = init::cursor_kaizen_hook_wiring(&ws);
158 match &cursor {
159 Ok(None) => writeln!(
160 &mut out,
161 "hooks: ~/.cursor/hooks.json — absent (run `kaizen init` to wire Cursor)"
162 )
163 .unwrap(),
164 Ok(Some(true)) => writeln!(
165 &mut out,
166 "hooks: ~/.cursor/hooks.json — kaizen command on all events"
167 )
168 .unwrap(),
169 Ok(Some(false)) => {
170 writeln!(&mut out, "hooks: ~/.cursor/hooks.json — present but not fully wired to kaizen (run: kaizen init)").unwrap();
171 }
172 Err(e) => writeln!(&mut out, "hooks: ~/.cursor/hooks.json — read error: {e}").unwrap(),
173 }
174 let claude = init::claude_kaizen_hook_wiring(&ws);
175 match &claude {
176 Ok(None) => writeln!(
177 &mut out,
178 "hooks: ~/.claude/settings.json — absent (run `kaizen init` to wire Claude Code)"
179 )
180 .unwrap(),
181 Ok(Some(true)) => writeln!(
182 &mut out,
183 "hooks: ~/.claude/settings.json — kaizen hooks on all events"
184 )
185 .unwrap(),
186 Ok(Some(false)) => {
187 writeln!(
188 &mut out,
189 "hooks: ~/.claude/settings.json — present but not fully wired (run: kaizen init)"
190 )
191 .unwrap();
192 }
193 Err(e) => {
194 writeln!(&mut out, "hooks: ~/.claude/settings.json — read error: {e}").unwrap();
195 }
196 }
197 for path in init::detect_legacy_wiring(&ws) {
198 writeln!(
199 &mut out,
200 "hooks: legacy local wiring at {} — safe to remove (kaizen now wires globally)",
201 path.display()
202 )
203 .unwrap();
204 }
205 let openclaw = init::openclaw_kaizen_hook_wiring(&ws);
206 match &openclaw {
207 Ok(None) => writeln!(
208 &mut out,
209 "hooks: ~/.openclaw/hooks/kaizen-events — absent (run `kaizen init` to wire OpenClaw)"
210 )
211 .unwrap(),
212 Ok(Some(true)) => {
213 writeln!(&mut out, "hooks: ~/.openclaw/hooks/kaizen-events — wired").unwrap()
214 }
215 Ok(Some(false)) => writeln!(
216 &mut out,
217 "hooks: ~/.openclaw/hooks/kaizen-events — present but partial (run: kaizen init)"
218 )
219 .unwrap(),
220 Err(e) => writeln!(
221 &mut out,
222 "hooks: ~/.openclaw/hooks/kaizen-events — read error: {e}"
223 )
224 .unwrap(),
225 }
226 writeln!(&mut out).unwrap();
227 if std::io::stdout().is_terminal() {
228 writeln!(&mut out, "If sessions list is empty, run a short agent session in this repo and `kaizen sessions list` again; see https://github.com/marquesds/kaizen/blob/main/docs/config.md#sources.").unwrap();
229 } else {
230 writeln!(&mut out, "If sessions list is empty, see docs/config.md (sources) and `kaizen doctor` from a TTY for tips.").unwrap();
231 }
232 if hard_fail {
233 Ok((1, out))
234 } else {
235 Ok((0, out))
236 }
237}
238
239pub fn cmd_doctor(workspace: Option<&Path>) -> Result<i32> {
240 let (code, s) = doctor_text(workspace)?;
241 print!("{s}");
242 Ok(code)
243}