1use std::collections::HashMap;
4use std::path::Path;
5use std::process::Command;
6
7use anyhow::{Context, Result};
8use serde::Serialize;
9
10use mi6_core::{Config, all_adapters};
11
12use super::upgrade::InstallMethod;
13use crate::display::{bold_green, bold_red, bold_white, bold_yellow, dark_grey, format_bytes};
14
15#[derive(Serialize)]
17struct StatusOutput {
18 version: String,
19 #[serde(skip_serializing_if = "Option::is_none")]
20 commit: Option<String>,
21 frameworks: HashMap<String, FrameworkStatus>,
22 data: DataStatus,
23}
24
25#[derive(Serialize)]
26struct FrameworkStatus {
27 activated: bool,
28 #[serde(skip_serializing_if = "Option::is_none")]
29 enabled_via: Option<String>,
30 #[serde(skip_serializing_if = "Option::is_none")]
31 config_path: Option<String>,
32 #[serde(skip_serializing_if = "Option::is_none")]
33 verified: Option<VerificationResult>,
34}
35
36#[derive(Clone, Serialize)]
38struct VerificationResult {
39 responding: bool,
41 message: String,
43}
44
45#[derive(Serialize)]
46struct DataStatus {
47 storage_bytes: u64,
48 storage_human: String,
49 path: String,
50}
51
52fn get_dir_size(path: &Path) -> std::io::Result<u64> {
54 let mut total = 0;
55
56 if path.is_file() {
57 return Ok(path.metadata()?.len());
58 }
59
60 if path.is_dir() {
61 for entry in std::fs::read_dir(path)? {
62 let entry = entry?;
63 let path = entry.path();
64 if path.is_file() {
65 total += path.metadata()?.len();
66 } else if path.is_dir() {
67 total += get_dir_size(&path)?;
68 }
69 }
70 }
71
72 Ok(total)
73}
74
75fn get_git_commit() -> Option<String> {
77 option_env!("MI6_GIT_COMMIT").map(String::from)
79}
80
81fn get_enablement_method(name: &str) -> &'static str {
83 if name == "claude" {
84 "plugin"
85 } else {
86 "config hooks"
87 }
88}
89
90fn verify_hooks() -> VerificationResult {
102 let result = Command::new("mi6")
104 .args(["ingest", "event", "--ping"])
105 .stdin(std::process::Stdio::null())
106 .stdout(std::process::Stdio::piped())
107 .stderr(std::process::Stdio::null())
108 .output();
109
110 match result {
111 Ok(output) if output.status.success() => {
112 let stdout = String::from_utf8_lossy(&output.stdout);
113 if stdout.trim() == "pong" {
114 VerificationResult {
115 responding: true,
116 message: "hooks responding".to_string(),
117 }
118 } else {
119 VerificationResult {
120 responding: false,
121 message: "unexpected response".to_string(),
122 }
123 }
124 }
125 Ok(_) => VerificationResult {
126 responding: false,
127 message: "command failed".to_string(),
128 },
129 Err(e) => {
130 let message = if e.kind() == std::io::ErrorKind::NotFound {
131 "mi6 not in PATH".to_string()
132 } else {
133 format!("error: {}", e)
134 };
135 VerificationResult {
136 responding: false,
137 message,
138 }
139 }
140 }
141}
142
143pub fn run_status_command(json: bool, verbose: bool, check: bool) -> Result<()> {
145 let version = env!("CARGO_PKG_VERSION").to_string();
147 let commit = get_git_commit();
148
149 let verification = if check { Some(verify_hooks()) } else { None };
151
152 let mut frameworks = HashMap::new();
153 for adapter in all_adapters() {
154 let activated = adapter.has_mi6_hooks(false, false);
155 let enabled_via = if activated {
156 Some(get_enablement_method(adapter.name()).to_string())
157 } else {
158 None
159 };
160 let config_path = if activated {
161 if adapter.name() == "claude" {
163 Config::mi6_dir()
164 .ok()
165 .map(|d| d.join("claude-plugin").display().to_string())
166 } else {
167 adapter
168 .settings_path(false, false)
169 .ok()
170 .map(|p| p.display().to_string())
171 }
172 } else {
173 None
174 };
175
176 let verified = if activated {
178 verification.clone()
179 } else {
180 None
181 };
182
183 frameworks.insert(
184 adapter.name().to_string(),
185 FrameworkStatus {
186 activated,
187 enabled_via,
188 config_path,
189 verified,
190 },
191 );
192 }
193
194 let db_path = mi6_core::Config::db_path().context("failed to determine database path")?;
195 let storage_bytes = if db_path.exists() {
196 get_dir_size(&db_path).unwrap_or(0)
197 } else {
198 0
199 };
200
201 if json {
202 let output = StatusOutput {
204 version,
205 commit,
206 frameworks,
207 data: DataStatus {
208 storage_bytes,
209 storage_human: format_bytes(storage_bytes),
210 path: db_path.display().to_string(),
211 },
212 };
213 println!("{}", serde_json::to_string_pretty(&output)?);
214 } else {
215 print!("mi6 {}", bold_white(&version));
217 if let Some(ref c) = commit {
218 print!(" ({})", bold_white(c));
219 }
220 println!();
221
222 if verbose {
224 println!();
225 let method_str = match InstallMethod::detect() {
226 Ok(method) => method.name().to_string(),
227 Err(_) => "unknown".to_string(),
228 };
229 println!(
230 "{} {}",
231 dark_grey("mi6 installation method:"),
232 bold_white(&method_str)
233 );
234 }
235 println!();
236
237 let mi6_dir = Config::mi6_dir().context("failed to determine mi6 directory")?;
239 println!(
240 "{} {}",
241 dark_grey("mi6 directory:"),
242 bold_white(&mi6_dir.display().to_string())
243 );
244 println!();
245
246 let adapters: Vec<_> = all_adapters();
248 let framework_width = adapters
249 .iter()
250 .map(|a| a.display_name().len())
251 .max()
252 .unwrap_or(10)
253 .max("Framework".len());
254 let status_width = 13; let method_width = 12; let otel_width = 4; let verified_width = if check { 20 } else { 0 }; let sep = dark_grey(" │ ");
259
260 if check {
262 println!(
263 "{:<fw$}{}{:<sw$}{}{:<mw$}{}OTEL{}Verified",
264 "Framework",
265 sep,
266 "Status",
267 sep,
268 "Method",
269 sep,
270 sep,
271 fw = framework_width,
272 sw = status_width,
273 mw = method_width
274 );
275 } else {
276 println!(
277 "{:<fw$}{}{:<sw$}{}{:<mw$}{}OTEL",
278 "Framework",
279 sep,
280 "Status",
281 sep,
282 "Method",
283 sep,
284 fw = framework_width,
285 sw = status_width,
286 mw = method_width
287 );
288 }
289
290 if check {
292 println!(
293 "{}",
294 dark_grey(&format!(
295 "{}──┼──{}──┼──{}──┼──{}──┼──{}",
296 "─".repeat(framework_width),
297 "─".repeat(status_width),
298 "─".repeat(method_width),
299 "─".repeat(otel_width),
300 "─".repeat(verified_width)
301 ))
302 );
303 } else {
304 println!(
305 "{}",
306 dark_grey(&format!(
307 "{}──┼──{}──┼──{}──┼──{}",
308 "─".repeat(framework_width),
309 "─".repeat(status_width),
310 "─".repeat(method_width),
311 "─".repeat(otel_width)
312 ))
313 );
314 }
315
316 let mut has_verification_failure = false;
318
319 for adapter in &adapters {
321 let status_info = frameworks.get(adapter.name());
322 let enabled = status_info.is_some_and(|f| f.activated);
323 let installed = adapter.is_installed();
324
325 let (status, method, otel, verified_str) = if enabled {
327 let method = status_info
328 .and_then(|f| f.enabled_via.as_ref())
329 .cloned()
330 .unwrap_or_default();
331 let otel_str = match adapter.otel_support() {
333 mi6_core::framework::OtelSupport::Enabled => "yes",
334 mi6_core::framework::OtelSupport::Disabled => "no",
335 mi6_core::framework::OtelSupport::Unsupported => "n/a",
336 };
337
338 let verified = if check {
340 if let Some(v) = status_info.and_then(|f| f.verified.as_ref()) {
341 if v.responding {
342 bold_green(&v.message)
343 } else {
344 has_verification_failure = true;
345 bold_yellow(&format!("{} (!)", v.message))
346 }
347 } else {
348 "-".to_string()
349 }
350 } else {
351 String::new()
352 };
353
354 (
355 bold_green(&format!("{:<sw$}", "ENABLED", sw = status_width)),
356 method,
357 otel_str,
358 verified,
359 )
360 } else if !installed {
361 (
362 dark_grey(&format!("{:<sw$}", "NOT INSTALLED", sw = status_width)),
363 "-".to_string(),
364 "n/a",
365 if check {
366 "-".to_string()
367 } else {
368 String::new()
369 },
370 )
371 } else {
372 (
373 bold_red(&format!("{:<sw$}", "NOT ENABLED", sw = status_width)),
374 "-".to_string(),
375 "n/a",
376 if check {
377 "-".to_string()
378 } else {
379 String::new()
380 },
381 )
382 };
383
384 if check {
385 println!(
386 "{:<fw$}{}{}{}{:<mw$}{}{}{}{}",
387 adapter.display_name(),
388 sep,
389 status,
390 sep,
391 method,
392 sep,
393 otel,
394 sep,
395 verified_str,
396 fw = framework_width,
397 mw = method_width
398 );
399 } else {
400 println!(
401 "{:<fw$}{}{}{}{:<mw$}{}{}",
402 adapter.display_name(),
403 sep,
404 status,
405 sep,
406 method,
407 sep,
408 otel,
409 fw = framework_width,
410 mw = method_width
411 );
412 }
413 }
414 println!();
415
416 if check && has_verification_failure {
418 println!(
419 "{}",
420 bold_yellow("Warning: Some hooks are configured but not responding.")
421 );
422 println!(" - Check that mi6 is in your PATH");
423 println!(" - Run: which mi6");
424 println!();
425 }
426
427 if verbose {
429 let enabled_with_paths: Vec<_> = adapters
431 .iter()
432 .filter_map(|a| {
433 frameworks.get(a.name()).and_then(|f| {
434 if f.activated {
435 f.config_path.as_ref().map(|p| (a.name(), p.as_str()))
436 } else {
437 None
438 }
439 })
440 })
441 .collect();
442
443 if !enabled_with_paths.is_empty() {
444 println!("{}", bold_white("Relevant Framework Files"));
445 for (name, path) in enabled_with_paths {
446 println!("- {}: {}", name, path);
447 }
448 println!();
449 }
450 }
451
452 println!(
453 "{}{}{}{}{}",
454 dark_grey("(modify using "),
455 bold_white("mi6 enable <framework>"),
456 dark_grey(" or "),
457 bold_white("mi6 disable <framework>"),
458 dark_grey(")")
459 );
460 }
461
462 Ok(())
463}