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