1use crate::cli::DoctorArgs;
4use crate::errors::CliError;
5use crate::output::terminal;
6use crate::output::{CommandResult, HumanRenderable, OutputContext};
7use serde::Serialize;
8
9#[derive(Debug, Serialize)]
11pub struct DoctorResult {
12 pub checks: Vec<DoctorCheck>,
14 pub all_passed: bool,
16 pub error_count: usize,
18 pub warning_count: usize,
20}
21
22#[derive(Debug, Serialize, Clone)]
24pub struct DoctorCheck {
25 pub category: String,
27 pub name: String,
29 pub passed: bool,
31 pub detail: String,
33 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
74pub async fn execute(
78 args: &DoctorArgs,
79 _ctx: &OutputContext,
80) -> Result<CommandResult<DoctorResult>, CliError> {
81 let mut checks = Vec::new();
82
83 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 checks.push(check_command_version(
94 "Rust Toolchain",
95 "rustc",
96 &["--version"],
97 ));
98
99 checks.push(check_wasm_target(args.fix));
101
102 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 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 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
158fn 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
184fn 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
240fn 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}