Skip to main content

torvyn_cli/commands/
doctor.rs

1//! `torvyn doctor` — check development environment.
2
3use crate::cli::DoctorArgs;
4use crate::errors::CliError;
5use crate::output::terminal;
6use crate::output::{CommandResult, HumanRenderable, OutputContext};
7use serde::Serialize;
8
9/// Result of `torvyn doctor`.
10#[derive(Debug, Serialize)]
11pub struct DoctorResult {
12    /// All checks performed.
13    pub checks: Vec<DoctorCheck>,
14    /// Whether all checks passed.
15    pub all_passed: bool,
16    /// Number of failing checks.
17    pub error_count: usize,
18    /// Number of warnings.
19    pub warning_count: usize,
20}
21
22/// A single doctor check.
23#[derive(Debug, Serialize, Clone)]
24pub struct DoctorCheck {
25    /// Category (e.g., "Rust Toolchain", "WebAssembly Tools").
26    pub category: String,
27    /// Tool or check name.
28    pub name: String,
29    /// Whether the check passed.
30    pub passed: bool,
31    /// Detail string (version info, etc.).
32    pub detail: String,
33    /// Fix suggestion if the check failed.
34    pub fix: Option<String>,
35}
36
37impl HumanRenderable for DoctorResult {
38    fn render_human(&self, ctx: &OutputContext) {
39        let mut current_category = String::new();
40        for check in &self.checks {
41            if check.category != current_category {
42                eprintln!();
43                eprintln!("  {}", check.category);
44                current_category.clone_from(&check.category);
45            }
46
47            if check.passed {
48                terminal::print_success(ctx, &format!("{} {}", check.name, check.detail));
49            } else {
50                terminal::print_failure(ctx, &format!("{} {}", check.name, check.detail));
51                if let Some(fix) = &check.fix {
52                    eprintln!();
53                    if ctx.color_enabled {
54                        eprintln!("      {} {}", console::style("fix:").cyan().bold(), fix);
55                    } else {
56                        eprintln!("      fix: {fix}");
57                    }
58                }
59            }
60        }
61
62        eprintln!();
63        if self.all_passed {
64            eprintln!("  All checks passed!");
65        } else {
66            eprintln!(
67                "  {} error(s), {} warning(s). Run `torvyn doctor --fix` to attempt automatic repair.",
68                self.error_count, self.warning_count
69            );
70        }
71    }
72}
73
74/// Execute the `torvyn doctor` command.
75///
76/// COLD PATH.
77pub async fn execute(
78    args: &DoctorArgs,
79    _ctx: &OutputContext,
80) -> Result<CommandResult<DoctorResult>, CliError> {
81    let mut checks = Vec::new();
82
83    // Check 1: Torvyn CLI version
84    checks.push(DoctorCheck {
85        category: "Torvyn CLI".into(),
86        name: "torvyn".into(),
87        passed: true,
88        detail: format!("{} (up to date)", env!("CARGO_PKG_VERSION")),
89        fix: None,
90    });
91
92    // Check 2: Rust toolchain — rustc
93    checks.push(check_command_version(
94        "Rust Toolchain",
95        "rustc",
96        &["--version"],
97    ));
98
99    // Check 3: wasm32-wasip2 target
100    checks.push(check_wasm_target(args.fix));
101
102    // Check 4: cargo-component
103    checks.push(check_command_existence(
104        "Rust Toolchain",
105        "cargo-component",
106        &["cargo", "component", "--version"],
107        Some("Run `cargo install cargo-component`"),
108        args.fix,
109        Some(&["cargo", "install", "cargo-component"]),
110    ));
111
112    // Check 5: wasm-tools
113    checks.push(check_command_existence(
114        "WebAssembly Tools",
115        "wasm-tools",
116        &["wasm-tools", "--version"],
117        Some("Run `cargo install wasm-tools`"),
118        args.fix,
119        Some(&["cargo", "install", "wasm-tools"]),
120    ));
121
122    // Check 6: Project-specific — Torvyn.toml
123    let torvyn_toml_exists = std::path::Path::new("./Torvyn.toml").exists();
124    checks.push(DoctorCheck {
125        category: "Project".into(),
126        name: "Torvyn.toml".into(),
127        passed: torvyn_toml_exists,
128        detail: if torvyn_toml_exists {
129            "found".into()
130        } else {
131            "NOT found (not in a Torvyn project directory)".into()
132        },
133        fix: if torvyn_toml_exists {
134            None
135        } else {
136            Some("Run `torvyn init` to create a project.".into())
137        },
138    });
139
140    let error_count = checks.iter().filter(|c| !c.passed).count();
141    let all_passed = error_count == 0;
142
143    let result = DoctorResult {
144        checks,
145        all_passed,
146        error_count,
147        warning_count: 0,
148    };
149
150    Ok(CommandResult {
151        success: true,
152        command: "doctor".into(),
153        data: result,
154        warnings: vec![],
155    })
156}
157
158/// Check if a command exists and get its version.
159fn check_command_version(category: &str, name: &str, args: &[&str]) -> DoctorCheck {
160    match std::process::Command::new(args[0])
161        .args(&args[1..])
162        .output()
163    {
164        Ok(output) => {
165            let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
166            DoctorCheck {
167                category: category.into(),
168                name: name.into(),
169                passed: true,
170                detail: version,
171                fix: None,
172            }
173        }
174        Err(_) => DoctorCheck {
175            category: category.into(),
176            name: name.into(),
177            passed: false,
178            detail: "NOT found".into(),
179            fix: Some(format!("Install {name}")),
180        },
181    }
182}
183
184/// Check if a command exists, optionally auto-fix by installing.
185fn check_command_existence(
186    category: &str,
187    name: &str,
188    check_args: &[&str],
189    fix_hint: Option<&str>,
190    attempt_fix: bool,
191    fix_cmd: Option<&[&str]>,
192) -> DoctorCheck {
193    let output = std::process::Command::new(check_args[0])
194        .args(&check_args[1..])
195        .output();
196
197    match output {
198        Ok(o) if o.status.success() => {
199            let version = String::from_utf8_lossy(&o.stdout).trim().to_string();
200            DoctorCheck {
201                category: category.into(),
202                name: name.into(),
203                passed: true,
204                detail: version,
205                fix: None,
206            }
207        }
208        _ => {
209            if attempt_fix {
210                if let Some(cmd) = fix_cmd {
211                    let fix_result = std::process::Command::new(cmd[0])
212                        .args(&cmd[1..])
213                        .stdout(std::process::Stdio::null())
214                        .stderr(std::process::Stdio::null())
215                        .status();
216
217                    if fix_result.map(|s| s.success()).unwrap_or(false) {
218                        return DoctorCheck {
219                            category: category.into(),
220                            name: name.into(),
221                            passed: true,
222                            detail: "installed (auto-fixed)".into(),
223                            fix: None,
224                        };
225                    }
226                }
227            }
228
229            DoctorCheck {
230                category: category.into(),
231                name: name.into(),
232                passed: false,
233                detail: "not found".into(),
234                fix: fix_hint.map(|s| s.to_string()),
235            }
236        }
237    }
238}
239
240/// Check if the wasm32-wasip2 Rust target is installed.
241fn check_wasm_target(attempt_fix: bool) -> DoctorCheck {
242    let output = std::process::Command::new("rustup")
243        .args(["target", "list", "--installed"])
244        .output();
245
246    let target_installed = output
247        .as_ref()
248        .map(|o| {
249            String::from_utf8_lossy(&o.stdout)
250                .lines()
251                .any(|l| l.trim() == "wasm32-wasip2")
252        })
253        .unwrap_or(false);
254
255    if target_installed {
256        DoctorCheck {
257            category: "Rust Toolchain".into(),
258            name: "wasm32-wasip2 target".into(),
259            passed: true,
260            detail: "installed".into(),
261            fix: None,
262        }
263    } else {
264        if attempt_fix {
265            let fix_result = std::process::Command::new("rustup")
266                .args(["target", "add", "wasm32-wasip2"])
267                .stdout(std::process::Stdio::null())
268                .stderr(std::process::Stdio::null())
269                .status();
270
271            if fix_result.map(|s| s.success()).unwrap_or(false) {
272                return DoctorCheck {
273                    category: "Rust Toolchain".into(),
274                    name: "wasm32-wasip2 target".into(),
275                    passed: true,
276                    detail: "installed (auto-fixed)".into(),
277                    fix: None,
278                };
279            }
280        }
281
282        DoctorCheck {
283            category: "Rust Toolchain".into(),
284            name: "wasm32-wasip2 target".into(),
285            passed: false,
286            detail: "NOT installed".into(),
287            fix: Some("Run `rustup target add wasm32-wasip2`".into()),
288        }
289    }
290}