Skip to main content

oxicode/cli/commands/
doctor.rs

1//! `oxicode doctor` — home-layout diagnostics.
2//!
3//! Prints the active unified home resolution in one glance:
4//!
5//! - the Oxi home (`oxi_home()`) and the owned oxicode subtree
6//!   (`oxicode_home()`),
7//! - legacy `~/.oxicode` presence and whether a home-layout migration is
8//!   pending,
9//! - the well-known oxicode-owned paths derived from the canonical home.
10//!
11//! Read-only: doctor never mutates state.
12
13use anyhow::Result;
14use oxicode_catalog::oxi_home;
15use std::path::PathBuf;
16
17use super::reset::display_path;
18
19/// Journal status line for the doctor report.
20fn journal_status(journal_path: Option<PathBuf>) -> String {
21    let Some(path) = journal_path.filter(|p| p.exists()) else {
22        return "absent".to_string();
23    };
24    match crate::home_migrate::MigrationJournal::load(&path) {
25        Some(j) if j.is_in_progress() => "in_progress".to_string(),
26        Some(j) if j.status == "complete" => "complete".to_string(),
27        Some(_) => "present (unexpected status)".to_string(),
28        None => "present (unreadable)".to_string(),
29    }
30}
31
32/// Handle `oxicode doctor`. Prints home-layout diagnostics; exit code 0.
33pub fn handle_doctor() -> Result<()> {
34    println!("Oxi home layout");
35    println!("---------------");
36
37    let oxi = oxi_home::oxi_home();
38    let canonical = oxi_home::oxicode_home();
39    let legacy = oxi_home::legacy_home_dir();
40    let journal_path = oxi_home::migration_journal_path();
41
42    println!(
43        "  oxi home:        {}",
44        oxi.as_ref()
45            .map(|p| display_path(p))
46            .unwrap_or_else(|| "<unresolvable>".to_string())
47    );
48    println!(
49        "  oxicode home:    {}  (canonical, owned by oxicode)",
50        canonical
51            .as_ref()
52            .map(|p| display_path(p))
53            .unwrap_or_else(|| "<unresolvable>".to_string())
54    );
55    if let Some(env_val) = std::env::var_os("OXICODE_HOME") {
56        println!(
57            "    override:      OXICODE_HOME={}",
58            env_val.to_string_lossy()
59        );
60    } else if let Some(env_val) = std::env::var_os("OXI_HOME") {
61        println!("    override:      OXI_HOME={}", env_val.to_string_lossy());
62    }
63
64    match &legacy {
65        Some(legacy_dir) => {
66            println!(
67                "  legacy home:     {}  (present, read-only)",
68                display_path(legacy_dir)
69            );
70            let complete = journal_status(journal_path.clone()) == "complete";
71            if canonical.as_ref().is_some_and(|c| c.exists()) && complete {
72                println!("  migration:       complete");
73            } else {
74                println!("  migration:       pending  (run `oxicode migrate home`)");
75            }
76        }
77        None => {
78            println!("  legacy home:     absent");
79            println!("  migration:       nothing to do");
80        }
81    }
82    println!(
83        "  journal:         {}",
84        journal_status(journal_path).replace(
85            "in_progress",
86            "in_progress (rerun `oxicode migrate home` to resume)"
87        )
88    );
89
90    // Well-known owned paths (canonical home when resolvable).
91    if let Some(home) = canonical {
92        println!();
93        println!("Owned paths");
94        println!("-----------");
95        for (label, rel) in [
96            ("settings", "settings.json"),
97            ("auth", "auth.json"),
98            ("sessions", "sessions"),
99            ("skills", "skills"),
100            ("extensions", "extensions"),
101            ("packages", "packages"),
102            ("catalog overrides", "catalog/overrides.toml"),
103            ("models.dev cache", "cache/models-dev.json"),
104        ] {
105            println!(
106                "  {:<18} {}",
107                format!("{label}:"),
108                display_path(&home.join(rel))
109            );
110        }
111    }
112
113    // Project-local `.oxicode/` is a separate namespace — mention it when
114    // present so users don't confuse it with the home.
115    if let Ok(cwd) = std::env::current_dir() {
116        let project = cwd.join(".oxicode");
117        if project.is_dir() {
118            println!();
119            println!(
120                "  note: project-local .oxicode/ found at {} (separate namespace, not migrated)",
121                display_path(&project)
122            );
123        }
124    }
125
126    Ok(())
127}