From 3eff914e60d1edec950e130fbf99feec0c9200a4 Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Wed, 1 Apr 2026 15:23:13 +1100 Subject: [PATCH] refactor: consume shared doctor crate from builderbot (#38) * refactor: consume shared doctor crate from builderbot Replace the local doctor module (agents.rs, checks.rs, mod.rs, resolve.rs, types.rs) with thin Tauri command wrappers that delegate to the shared doctor crate extracted into block/builderbot. - Add doctor git dependency pointing to the doctor branch in builderbot - Delete src-tauri/src/commands/doctor/ directory (5 files, ~37KB) - Create src-tauri/src/commands/doctor.rs with run_doctor and run_doctor_fix wrappers plus pub use re-exports of shared types - No changes needed to commands/mod.rs (Rust resolves doctor.rs automatically) or lib.rs (same command function names) * build: pin doctor dependency to merged commit Switch the doctor crate from tracking the `doctor` branch to a specific rev pin (bfce292) now that the branch has been merged into main. Co-Authored-By: Claude Opus 4.6 (1M context) * fix: remove unused doctor imports flagged by clippy Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- ui/goose2/src-tauri/Cargo.lock | 127 ++--- ui/goose2/src-tauri/Cargo.toml | 1 + ui/goose2/src-tauri/src/commands/doctor.rs | 15 + .../src-tauri/src/commands/doctor/agents.rs | 254 ---------- .../src-tauri/src/commands/doctor/checks.rs | 438 ------------------ .../src-tauri/src/commands/doctor/mod.rs | 187 -------- .../src-tauri/src/commands/doctor/resolve.rs | 74 --- .../src-tauri/src/commands/doctor/types.rs | 65 --- 8 files changed, 81 insertions(+), 1080 deletions(-) create mode 100644 ui/goose2/src-tauri/src/commands/doctor.rs delete mode 100644 ui/goose2/src-tauri/src/commands/doctor/agents.rs delete mode 100644 ui/goose2/src-tauri/src/commands/doctor/checks.rs delete mode 100644 ui/goose2/src-tauri/src/commands/doctor/mod.rs delete mode 100644 ui/goose2/src-tauri/src/commands/doctor/resolve.rs delete mode 100644 ui/goose2/src-tauri/src/commands/doctor/types.rs diff --git a/ui/goose2/src-tauri/Cargo.lock b/ui/goose2/src-tauri/Cargo.lock index 715f999200..2523a9b10e 100644 --- a/ui/goose2/src-tauri/Cargo.lock +++ b/ui/goose2/src-tauri/Cargo.lock @@ -27,9 +27,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "agent-client-protocol" -version = "0.10.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e5bbb6e9d91077607468af8b5bb0f75d10d0b8ac0db49a0c46542cba8ff9f88" +checksum = "10eeef5e80864f9c3c148a3f395c3e35a66d37ec7561c7845b2bffae8e841759" dependencies = [ "agent-client-protocol-schema", "anyhow", @@ -44,9 +44,9 @@ dependencies = [ [[package]] name = "agent-client-protocol-schema" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96daddd0d00f2eab88f8099d38190881bf8d6c5e46b6fa21751f482775d0dba7" +checksum = "ca68e7e55681ce56546c0cecc6bc8f20493d24b44c6d93ec46174f310730bba2" dependencies = [ "anyhow", "derive_more 2.1.1", @@ -273,9 +273,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.39.0" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a" +checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399" dependencies = [ "cc", "cmake", @@ -561,9 +561,9 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ "cc", ] @@ -872,7 +872,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -921,6 +921,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "doctor" +version = "0.1.0" +source = "git+https://github.com/block/builderbot?rev=bfce29264181236a2e5b4fd903ace6291f6a7594#bfce29264181236a2e5b4fd903ace6291f6a7594" +dependencies = [ + "serde", + "tokio", +] + [[package]] name = "dom_query" version = "0.27.0" @@ -1052,7 +1061,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1562,6 +1571,7 @@ dependencies = [ "async-trait", "chrono", "dirs", + "doctor", "futures", "log", "rand 0.10.0", @@ -1759,9 +1769,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", @@ -1773,7 +1783,6 @@ dependencies = [ "httparse", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -2008,9 +2017,9 @@ checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8e7418f59cc01c88316161279a7f665217ae316b388e58a0d10e29f54f1e5eb" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" dependencies = [ "memchr", "serde", @@ -2120,9 +2129,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.92" +version = "0.3.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4c90f45aa2e6eacbe8645f77fdea542ac97a494bcd117a67df9ff4d611f995" +checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" dependencies = [ "cfg-if", "futures-util", @@ -2887,12 +2896,6 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "piper" version = "0.2.5" @@ -3016,7 +3019,7 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.25.8+spec-1.1.0", + "toml_edit 0.25.9+spec-1.1.0", ] [[package]] @@ -3452,7 +3455,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3509,7 +3512,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3788,9 +3791,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -3944,7 +3947,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4543,7 +4546,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4747,7 +4750,7 @@ checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ "indexmap 2.13.0", "serde_core", - "serde_spanned 1.1.0", + "serde_spanned 1.1.1", "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", @@ -4774,9 +4777,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.1.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] @@ -4807,30 +4810,30 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.8+spec-1.1.0" +version = "0.25.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16bff38f1d86c47f9ff0647e6838d7bb362522bdf44006c7068c2b1e606f1f3c" +checksum = "da053d28fe57e2c9d21b48261e14e7b4c8b670b54d2c684847b91feaf4c7dac5" dependencies = [ "indexmap 2.13.0", - "toml_datetime 1.1.0+spec-1.1.0", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 1.0.0", + "winnow 1.0.1", ] [[package]] name = "toml_parser" -version = "1.1.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" +checksum = "39ca317ebc49f06bd748bfba29533eac9485569dc9bf80b849024b025e814fb9" dependencies = [ - "winnow 1.0.0", + "winnow 1.0.1", ] [[package]] name = "toml_writer" -version = "1.1.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "tower" @@ -4956,7 +4959,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -5171,9 +5174,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.115" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6523d69017b7633e396a89c5efab138161ed5aafcbc8d3e5c5a42ae38f50495a" +checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" dependencies = [ "cfg-if", "once_cell", @@ -5184,9 +5187,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.65" +version = "0.4.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d1faf851e778dfa54db7cd438b70758eba9755cb47403f3496edd7c8fc212f0" +checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e" dependencies = [ "js-sys", "wasm-bindgen", @@ -5194,9 +5197,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.115" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3a6c758eb2f701ed3d052ff5737f5bfe6614326ea7f3bbac7156192dc32e67" +checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5204,9 +5207,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.115" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "921de2737904886b52bcbb237301552d05969a6f9c40d261eb0533c8b055fedf" +checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" dependencies = [ "bumpalo", "proc-macro2", @@ -5217,9 +5220,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.115" +version = "0.2.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a93e946af942b58934c604527337bad9ae33ba1d5c6900bbb41c2c07c2364a93" +checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" dependencies = [ "unicode-ident", ] @@ -5273,9 +5276,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.92" +version = "0.3.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84cde8507f4d7cfcb1185b8cb5890c494ffea65edbe1ba82cfd63661c805ed94" +checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" dependencies = [ "js-sys", "wasm-bindgen", @@ -5414,7 +5417,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -5857,9 +5860,9 @@ dependencies = [ [[package]] name = "winnow" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" dependencies = [ "memchr", ] @@ -6119,18 +6122,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.47" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.47" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", diff --git a/ui/goose2/src-tauri/Cargo.toml b/ui/goose2/src-tauri/Cargo.toml index c334e5b54a..ca2ec126b5 100644 --- a/ui/goose2/src-tauri/Cargo.toml +++ b/ui/goose2/src-tauri/Cargo.toml @@ -32,3 +32,4 @@ tokio-stream = "0.1" tokio-util = { version = "0.7", features = ["rt"] } async-trait = "0.1" acp-client = { git = "https://github.com/block/builderbot", rev = "dbd5bc9c0cd559c7a67ab2d56545dba110f4a4cb" } +doctor = { git = "https://github.com/block/builderbot", rev = "bfce29264181236a2e5b4fd903ace6291f6a7594" } diff --git a/ui/goose2/src-tauri/src/commands/doctor.rs b/ui/goose2/src-tauri/src/commands/doctor.rs new file mode 100644 index 0000000000..985af1b9c4 --- /dev/null +++ b/ui/goose2/src-tauri/src/commands/doctor.rs @@ -0,0 +1,15 @@ +//! Tauri command wrappers for the doctor health-check system. + +pub use doctor::{DoctorReport, FixType}; + +/// Run all health checks and return the report. +#[tauri::command] +pub async fn run_doctor() -> DoctorReport { + doctor::run_checks().await +} + +/// Run a fix command for a doctor check, identified by check ID and fix type. +#[tauri::command] +pub async fn run_doctor_fix(check_id: String, fix_type: FixType) -> Result<(), String> { + doctor::execute_fix(check_id, fix_type).await +} diff --git a/ui/goose2/src-tauri/src/commands/doctor/agents.rs b/ui/goose2/src-tauri/src/commands/doctor/agents.rs deleted file mode 100644 index 6252653042..0000000000 --- a/ui/goose2/src-tauri/src/commands/doctor/agents.rs +++ /dev/null @@ -1,254 +0,0 @@ -//! AI agent checks and fix command lookup. - -use std::process::Command; - -use super::resolve::format_command_output; -use super::types::{CheckStatus, DoctorCheck, FixType, ResolvedBinary}; - -/// Metadata for an individual AI agent check. -pub(super) struct AgentCheckInfo { - /// Check ID used in the doctor report, e.g. "ai-agent-goose". - pub id: &'static str, - /// Human-readable label, e.g. "Goose". - pub label: &'static str, - /// ACP bridge binary names to search for (first entry is preferred/current). - pub commands: &'static [&'static str], - /// Main CLI tool name (e.g. "claude"), if separate from the ACP bridge. - pub main_command: Option<&'static str>, - /// URL to install the main tool. - pub install_url: Option<&'static str>, - /// Shell command to install the main tool. - pub install_command: Option<&'static str>, - /// URL to install the ACP bridge, when the main tool is present but the bridge is not. - pub bridge_install_url: Option<&'static str>, - /// Shell command to install the ACP bridge (used as fix_command for partial installs). - pub bridge_install_command: Option<&'static str>, -} - -/// All AI agents we check for individually. -pub(super) const AI_AGENT_CHECKS: &[AgentCheckInfo] = &[ - AgentCheckInfo { - id: "ai-agent-goose", - label: "Goose", - commands: &["goose"], - main_command: None, - install_url: Some("https://github.com/block/goose"), - install_command: None, - bridge_install_url: None, - bridge_install_command: None, - }, - AgentCheckInfo { - id: "ai-agent-claude", - label: "Claude Code", - commands: &["claude-agent-acp"], - main_command: Some("claude"), - install_url: Some("https://code.claude.com/docs/en/overview"), - install_command: Some("curl -fsSL https://claude.ai/install.sh | bash"), - bridge_install_url: Some("https://github.com/zed-industries/claude-agent-acp#installation"), - bridge_install_command: Some("npm install -g @zed-industries/claude-agent-acp"), - }, - AgentCheckInfo { - id: "ai-agent-codex", - label: "Codex", - commands: &["codex-acp"], - main_command: Some("codex"), - install_url: Some("https://github.com/openai/codex#quickstart"), - install_command: Some("brew install --cask codex"), - bridge_install_url: Some("https://github.com/zed-industries/codex-acp#installation"), - bridge_install_command: Some("npm install -g @zed-industries/codex-acp"), - }, - AgentCheckInfo { - id: "ai-agent-pi", - label: "Pi", - commands: &["pi-acp"], - main_command: Some("pi"), - install_url: None, - install_command: None, - bridge_install_url: None, - bridge_install_command: None, - }, - AgentCheckInfo { - id: "ai-agent-amp", - label: "Amp", - commands: &["amp-acp"], - main_command: Some("amp"), - install_url: Some("https://ampcode.com"), - install_command: Some("curl -fsSL https://ampcode.com/install.sh | bash"), - bridge_install_url: Some("https://www.npmjs.com/package/amp-acp"), - bridge_install_command: Some("npm install -g amp-acp"), - }, -]; - -/// Check whether a single AI agent is installed. -pub(super) fn check_single_ai_agent( - info: &AgentCheckInfo, - any_agent_found: bool, - resolved_cmds: &[ResolvedBinary], - resolved_main: Option<&ResolvedBinary>, -) -> DoctorCheck { - let header = format!( - "# Check: {} — verify {} agent is installed", - info.label, info.label - ); - let search_lines: Vec<&str> = resolved_cmds - .iter() - .map(|rb| rb.search_output.as_str()) - .collect(); - let search = search_lines.join("\n"); - - let resolved_path = resolved_cmds - .iter() - .find_map(|rb| rb.path.as_ref()) - .map(|p| p.to_string_lossy().to_string()); - - if let Some(ref path_str) = resolved_path { - if info.id == "ai-agent-goose" { - match Command::new(path_str).arg("acp").arg("--help").output() { - Ok(output) if output.status.success() => { - let raw = format!( - "{header}\n{}\n{}", - format_command_output("goose acp --help", &output), - search - ); - DoctorCheck { - id: info.id.to_string(), - label: info.label.to_string(), - status: CheckStatus::Pass, - message: "Installed".to_string(), - fix_url: None, - fix_command: None, - fix_type: None, - path: resolved_path, - bridge_path: None, - raw_output: Some(raw), - } - } - Ok(output) => { - let raw = format!( - "{header}\n{}\n{}", - format_command_output("goose acp --help", &output), - search - ); - DoctorCheck { - id: info.id.to_string(), - label: info.label.to_string(), - status: CheckStatus::Fail, - message: "Goose ACP subcommand not available — upgrade required" - .to_string(), - fix_url: Some("https://github.com/block/goose".to_string()), - fix_command: None, - fix_type: None, - path: resolved_path, - bridge_path: None, - raw_output: Some(raw), - } - } - Err(e) => DoctorCheck { - id: info.id.to_string(), - label: info.label.to_string(), - status: CheckStatus::Fail, - message: "Goose ACP subcommand not available — upgrade required".to_string(), - fix_url: Some("https://github.com/block/goose".to_string()), - fix_command: None, - fix_type: None, - path: resolved_path, - bridge_path: None, - raw_output: Some(format!( - "{header}\n$ goose acp --help\nerror: {e}\n{search}" - )), - }, - } - } else { - let (main_path, bridge_path) = if info.main_command.is_some() { - let main_p = resolved_main - .and_then(|rb| rb.path.as_ref()) - .map(|p| p.to_string_lossy().to_string()); - (main_p, resolved_path) - } else { - (resolved_path, None) - }; - DoctorCheck { - id: info.id.to_string(), - label: info.label.to_string(), - status: CheckStatus::Pass, - message: "Installed".to_string(), - fix_url: None, - fix_command: None, - fix_type: None, - path: main_path, - bridge_path, - raw_output: Some(format!("{header}\n{search}")), - } - } - } else { - // Bridge binary not found. If the main binary exists, suggest installing - // just the bridge; otherwise report the agent as not installed. - if let Some(main_path) = resolved_main.as_ref().and_then(|rm| rm.path.as_ref()) { - let bridge_cmd = info.commands[0]; - let main_search = &resolved_main.as_ref().unwrap().search_output; - return DoctorCheck { - id: info.id.to_string(), - label: info.label.to_string(), - status: CheckStatus::Warn, - message: format!( - "{} is installed but {} also needs to be installed", - info.label, bridge_cmd - ), - fix_url: info - .bridge_install_url - .or(info.install_url) - .map(|s| s.to_string()), - fix_command: info.bridge_install_command.map(|s| s.to_string()), - fix_type: info.bridge_install_command.map(|_| FixType::Bridge), - path: Some(main_path.to_string_lossy().to_string()), - bridge_path: None, - raw_output: Some(format!("{header}\n{search}\n{main_search}")), - }; - } - - // Neither bridge nor main binary found — agent is not installed. - let extra_search = resolved_main - .as_ref() - .map(|rm| format!("\n{}", rm.search_output)) - .unwrap_or_default(); - - DoctorCheck { - id: info.id.to_string(), - label: info.label.to_string(), - status: CheckStatus::Warn, - message: if any_agent_found { - "Not installed (optional)".to_string() - } else { - "Not installed — at least one AI agent is needed".to_string() - }, - fix_url: info.install_url.map(|s| s.to_string()), - fix_command: info.install_command.map(|s| s.to_string()), - fix_type: info.install_command.map(|_| FixType::Command), - path: None, - bridge_path: None, - raw_output: Some(format!("{header}\n{search}{extra_search}")), - } - } -} - -/// Look up the shell command for a given check ID and fix type. -/// -/// Returns `None` if the check ID is unknown or has no fix of the requested type. -pub(super) fn lookup_fix_command(check_id: &str, fix_type: &FixType) -> Option { - // Tool checks with hardcoded fix commands - if check_id == "git-clonefile" && *fix_type == FixType::Command { - return Some("git config --global core.clonefile true".to_string()); - } - - // AI agent checks - for info in AI_AGENT_CHECKS { - if info.id == check_id { - return match fix_type { - FixType::Command => info.install_command.map(|s| s.to_string()), - FixType::Bridge => info.bridge_install_command.map(|s| s.to_string()), - }; - } - } - - None -} diff --git a/ui/goose2/src-tauri/src/commands/doctor/checks.rs b/ui/goose2/src-tauri/src/commands/doctor/checks.rs deleted file mode 100644 index f756abe35c..0000000000 --- a/ui/goose2/src-tauri/src/commands/doctor/checks.rs +++ /dev/null @@ -1,438 +0,0 @@ -//! Individual health-check functions for tool dependencies (git, gh, git-lfs, etc.). - -use std::process::Command; - -use super::resolve::format_command_output; -use super::types::{CheckStatus, DoctorCheck, FixType, ResolvedBinary}; - -/// Check that `git` is installed and reachable. -pub(super) fn check_git(resolved: &ResolvedBinary) -> DoctorCheck { - let label = "Git".to_string(); - let id = "git".to_string(); - let search = &resolved.search_output; - let header = "# Check: Git — verify git is installed and reachable"; - - let git_path = match &resolved.path { - Some(p) => p, - None => { - return DoctorCheck { - id, - label, - status: CheckStatus::Fail, - message: "Git not found".to_string(), - fix_url: Some("https://git-scm.com/downloads".to_string()), - fix_command: None, - fix_type: None, - path: None, - bridge_path: None, - raw_output: Some(format!("{header}\nnot found via resolve_binary\n{search}")), - }; - } - }; - let path_str = git_path.to_string_lossy().to_string(); - - match Command::new(git_path).arg("--version").output() { - Ok(output) if output.status.success() => { - let version = String::from_utf8_lossy(&output.stdout).trim().to_string(); - let raw = format!( - "{header}\n{}\n{}", - format_command_output("git --version", &output), - search - ); - DoctorCheck { - id, - label, - status: CheckStatus::Pass, - message: version, - fix_url: None, - fix_command: None, - fix_type: None, - path: Some(path_str), - bridge_path: None, - raw_output: Some(raw), - } - } - Ok(output) => { - let raw = format!( - "{header}\n{}\n{}", - format_command_output("git --version", &output), - search - ); - DoctorCheck { - id, - label, - status: CheckStatus::Fail, - message: "Git not found".to_string(), - fix_url: Some("https://git-scm.com/downloads".to_string()), - fix_command: None, - fix_type: None, - path: Some(path_str), - bridge_path: None, - raw_output: Some(raw), - } - } - Err(e) => DoctorCheck { - id, - label, - status: CheckStatus::Fail, - message: "Git not found".to_string(), - fix_url: Some("https://git-scm.com/downloads".to_string()), - fix_command: None, - fix_type: None, - path: Some(path_str), - bridge_path: None, - raw_output: Some(format!("{header}\n$ git --version\nerror: {e}\n{search}")), - }, - } -} - -/// Check that the GitHub CLI (`gh`) is installed. -pub(super) fn check_gh(resolved: &ResolvedBinary) -> DoctorCheck { - let label = "GitHub CLI".to_string(); - let id = "gh".to_string(); - let search = &resolved.search_output; - let header = "# Check: GitHub CLI — verify gh is installed"; - - let gh_path = match &resolved.path { - Some(p) => p, - None => { - return DoctorCheck { - id, - label, - status: CheckStatus::Fail, - message: "GitHub CLI not found".to_string(), - fix_url: Some("https://cli.github.com".to_string()), - fix_command: None, - fix_type: None, - path: None, - bridge_path: None, - raw_output: Some(format!("{header}\nnot found via resolve_binary\n{search}")), - }; - } - }; - let path_str = gh_path.to_string_lossy().to_string(); - - match Command::new(gh_path).arg("--version").output() { - Ok(output) if output.status.success() => { - let version = String::from_utf8_lossy(&output.stdout); - let first_line = version.lines().next().unwrap_or("gh").trim().to_string(); - let raw = format!( - "{header}\n{}\n{}", - format_command_output("gh --version", &output), - search - ); - DoctorCheck { - id, - label, - status: CheckStatus::Pass, - message: first_line, - fix_url: None, - fix_command: None, - fix_type: None, - path: Some(path_str), - bridge_path: None, - raw_output: Some(raw), - } - } - Ok(output) => { - let raw = format!( - "{header}\n{}\n{}", - format_command_output("gh --version", &output), - search - ); - DoctorCheck { - id, - label, - status: CheckStatus::Fail, - message: "GitHub CLI not found".to_string(), - fix_url: Some("https://cli.github.com".to_string()), - fix_command: None, - fix_type: None, - path: Some(path_str), - bridge_path: None, - raw_output: Some(raw), - } - } - Err(e) => DoctorCheck { - id, - label, - status: CheckStatus::Fail, - message: "GitHub CLI not found".to_string(), - fix_url: Some("https://cli.github.com".to_string()), - fix_command: None, - fix_type: None, - path: Some(path_str), - bridge_path: None, - raw_output: Some(format!("{header}\n$ gh --version\nerror: {e}\n{search}")), - }, - } -} - -/// Check that `gh auth status` succeeds (user is logged in). -pub(super) fn check_gh_auth(gh: &ResolvedBinary) -> DoctorCheck { - let label = "GitHub Auth".to_string(); - let id = "gh-auth".to_string(); - let header = "# Check: GitHub Auth — verify user is logged in to GitHub"; - - let gh_path = match &gh.path { - Some(p) => p, - None => { - return DoctorCheck { - id, - label, - status: CheckStatus::Fail, - message: "GitHub CLI not found — install gh first".to_string(), - fix_url: Some("https://cli.github.com".to_string()), - fix_command: None, - fix_type: None, - path: None, - bridge_path: None, - raw_output: Some(format!("{header}\ngh not found via resolve_binary")), - }; - } - }; - - match Command::new(gh_path).args(["auth", "status"]).output() { - Ok(output) => { - let raw = format!( - "{header}\n{}", - format_command_output("gh auth status", &output) - ); - if output.status.success() { - DoctorCheck { - id, - label, - status: CheckStatus::Pass, - message: "Authenticated".to_string(), - fix_url: None, - fix_command: None, - fix_type: None, - path: None, - bridge_path: None, - raw_output: Some(raw), - } - } else { - let stderr = String::from_utf8_lossy(&output.stderr); - let hint = if stderr.contains("not logged in") || stderr.contains("no oauth token") - { - "Not authenticated — run `gh auth login`".to_string() - } else { - "Not authenticated".to_string() - }; - DoctorCheck { - id, - label, - status: CheckStatus::Fail, - message: hint, - fix_url: Some("https://cli.github.com/manual/gh_auth_login".to_string()), - fix_command: None, - fix_type: None, - path: None, - bridge_path: None, - raw_output: Some(raw), - } - } - } - Err(e) => DoctorCheck { - id, - label, - status: CheckStatus::Fail, - message: "Not authenticated".to_string(), - fix_url: Some("https://cli.github.com/manual/gh_auth_login".to_string()), - fix_command: None, - fix_type: None, - path: None, - bridge_path: None, - raw_output: Some(format!("{header}\n$ gh auth status\nerror: {e}")), - }, - } -} - -/// Check that Git LFS is installed. -pub(super) fn check_git_lfs(git: &ResolvedBinary, git_lfs: &ResolvedBinary) -> DoctorCheck { - let label = "Git LFS".to_string(); - let id = "git-lfs".to_string(); - let search = &git_lfs.search_output; - let header = - "# Check: Git LFS — verify git-lfs is installed (optional, needed for large files)"; - - let git_path = match &git.path { - Some(p) => p, - None => { - return DoctorCheck { - id, - label, - status: CheckStatus::Warn, - message: "Git LFS not installed (optional, needed for large files)".to_string(), - fix_url: Some("https://git-lfs.com".to_string()), - fix_command: None, - fix_type: None, - path: None, - bridge_path: None, - raw_output: Some(format!( - "{header}\ngit not found via resolve_binary\n{search}" - )), - }; - } - }; - - match Command::new(git_path).args(["lfs", "version"]).output() { - Ok(output) if output.status.success() => { - let version = String::from_utf8_lossy(&output.stdout).trim().to_string(); - let path = git_lfs - .path - .as_ref() - .map(|p| p.to_string_lossy().to_string()); - let raw = format!( - "{header}\n{}\n{}", - format_command_output("git lfs version", &output), - search - ); - DoctorCheck { - id, - label, - status: CheckStatus::Pass, - message: version, - fix_url: None, - fix_command: None, - fix_type: None, - path, - bridge_path: None, - raw_output: Some(raw), - } - } - Ok(output) => { - let raw = format!( - "{header}\n{}\n{}", - format_command_output("git lfs version", &output), - search - ); - DoctorCheck { - id, - label, - status: CheckStatus::Warn, - message: "Git LFS not installed (optional, needed for large files)".to_string(), - fix_url: Some("https://git-lfs.com".to_string()), - fix_command: None, - fix_type: None, - path: None, - bridge_path: None, - raw_output: Some(raw), - } - } - Err(e) => DoctorCheck { - id, - label, - status: CheckStatus::Warn, - message: "Git LFS not installed (optional, needed for large files)".to_string(), - fix_url: Some("https://git-lfs.com".to_string()), - fix_command: None, - fix_type: None, - path: None, - bridge_path: None, - raw_output: Some(format!("{header}\n$ git lfs version\nerror: {e}\n{search}")), - }, - } -} - -/// Check that `core.clonefile` is enabled in the global git config. -pub(super) fn check_clonefile(git: &ResolvedBinary) -> DoctorCheck { - let label = "Copy on Write Git Clones".to_string(); - let id = "git-clonefile".to_string(); - let fix_cmd = "git config --global core.clonefile true".to_string(); - let header = "# Check: Copy on Write Git Clones — verify core.clonefile is enabled for disk space savings"; - - let git_path = match &git.path { - Some(p) => p, - None => { - return DoctorCheck { - id, - label, - status: CheckStatus::Warn, - message: "Git not found — cannot check clonefile setting".to_string(), - fix_url: Some("https://git-scm.com/downloads".to_string()), - fix_command: None, - fix_type: None, - path: None, - bridge_path: None, - raw_output: Some(format!("{header}\ngit not found via resolve_binary")), - }; - } - }; - - match Command::new(git_path) - .args(["config", "--global", "core.clonefile"]) - .output() - { - Ok(output) if output.status.success() => { - let raw = format!( - "{header}\n{}", - format_command_output("git config --global core.clonefile", &output) - ); - let value = String::from_utf8_lossy(&output.stdout) - .trim() - .to_lowercase(); - if value == "true" { - DoctorCheck { - id, - label, - status: CheckStatus::Pass, - message: "Enabled — reduces disk space used by new worktrees".to_string(), - fix_url: None, - fix_command: None, - fix_type: None, - path: None, - bridge_path: None, - raw_output: Some(raw), - } - } else { - DoctorCheck { - id, - label, - status: CheckStatus::Warn, - message: "Disabled — enable to reduce disk space used by new worktrees" - .to_string(), - fix_url: None, - fix_command: Some(fix_cmd), - fix_type: Some(FixType::Command), - path: None, - bridge_path: None, - raw_output: Some(raw), - } - } - } - // Key not set — treat as not enabled - Ok(output) => { - let raw = format!( - "{header}\n{}", - format_command_output("git config --global core.clonefile", &output) - ); - DoctorCheck { - id, - label, - status: CheckStatus::Warn, - message: "Not set — enable to reduce disk space used by new worktrees".to_string(), - fix_url: None, - fix_command: Some(fix_cmd), - fix_type: Some(FixType::Command), - path: None, - bridge_path: None, - raw_output: Some(raw), - } - } - Err(e) => DoctorCheck { - id, - label, - status: CheckStatus::Warn, - message: "Not set — enable to reduce disk space used by new worktrees".to_string(), - fix_url: None, - fix_command: Some(fix_cmd), - fix_type: Some(FixType::Command), - path: None, - bridge_path: None, - raw_output: Some(format!( - "{header}\n$ git config --global core.clonefile\nerror: {e}" - )), - }, - } -} diff --git a/ui/goose2/src-tauri/src/commands/doctor/mod.rs b/ui/goose2/src-tauri/src/commands/doctor/mod.rs deleted file mode 100644 index 5ad9db5718..0000000000 --- a/ui/goose2/src-tauri/src/commands/doctor/mod.rs +++ /dev/null @@ -1,187 +0,0 @@ -//! Health Check ("Doctor") — backend checks for external dependencies. -//! -//! Each check probes a single external dependency and returns a status -//! (pass / warn / fail) with a human-readable summary and an optional -//! URL the user can visit to install or configure the dependency. - -mod agents; -mod checks; -mod resolve; -mod types; - -pub use types::{CheckStatus, DoctorCheck, DoctorReport, FixType}; - -use std::collections::HashMap; -use std::process::Command; - -use agents::{check_single_ai_agent, lookup_fix_command, AI_AGENT_CHECKS}; -use checks::{check_clonefile, check_gh, check_gh_auth, check_git, check_git_lfs}; -use resolve::resolve_binary; -use types::ResolvedBinary; - -/// Fallback check returned when a spawn_blocking task panics. -fn empty_check(id: &str, label: &str) -> DoctorCheck { - DoctorCheck { - id: id.to_string(), - label: label.to_string(), - status: CheckStatus::Fail, - message: "Check failed to run".to_string(), - fix_url: None, - fix_command: None, - fix_type: None, - path: None, - bridge_path: None, - raw_output: None, - } -} - -/// Run all health checks and return the report. -#[tauri::command] -pub async fn run_doctor() -> DoctorReport { - let mut binary_names: Vec<&'static str> = vec!["git", "gh", "git-lfs"]; - for info in AI_AGENT_CHECKS { - for cmd in info.commands { - if !binary_names.contains(cmd) { - binary_names.push(cmd); - } - } - if let Some(main) = info.main_command { - if !binary_names.contains(&main) { - binary_names.push(main); - } - } - } - - let handles: Vec<_> = binary_names - .iter() - .map(|&name| tokio::task::spawn_blocking(move || (name, resolve_binary(name)))) - .collect(); - - let mut resolved: HashMap<&str, ResolvedBinary> = HashMap::new(); - for handle in handles { - if let Ok((name, rb)) = handle.await { - resolved.insert(name, rb); - } - } - - let fallback = ResolvedBinary { - path: None, - search_output: "resolution task panicked".to_string(), - }; - let r_git = resolved - .get("git") - .cloned() - .unwrap_or_else(|| fallback.clone()); - let r_gh = resolved - .get("gh") - .cloned() - .unwrap_or_else(|| fallback.clone()); - let r_git_lfs = resolved - .get("git-lfs") - .cloned() - .unwrap_or_else(|| fallback.clone()); - - let any_agent_found = AI_AGENT_CHECKS.iter().any(|info| { - info.commands - .iter() - .any(|cmd| resolved.get(cmd).is_some_and(|rb| rb.path.is_some())) - }); - - let git_r = r_git.clone(); - let gh_r = r_gh.clone(); - let gh_r2 = r_gh.clone(); - let git_r2 = r_git.clone(); - let git_lfs_r = r_git_lfs; - let git_r3 = r_git; - - let c_git = tokio::task::spawn_blocking(move || check_git(&git_r)); - let c_gh = tokio::task::spawn_blocking(move || check_gh(&gh_r)); - let c_gh_auth = tokio::task::spawn_blocking(move || check_gh_auth(&gh_r2)); - let c_git_lfs = tokio::task::spawn_blocking(move || check_git_lfs(&git_r2, &git_lfs_r)); - let c_clonefile = tokio::task::spawn_blocking(move || check_clonefile(&git_r3)); - - let agent_handles: Vec<_> = AI_AGENT_CHECKS - .iter() - .map(|info| { - let found = any_agent_found; - let cmds: Vec = info - .commands - .iter() - .map(|cmd| { - resolved - .get(cmd) - .cloned() - .unwrap_or_else(|| fallback.clone()) - }) - .collect(); - let main = info.main_command.and_then(|cmd| resolved.get(cmd).cloned()); - tokio::task::spawn_blocking(move || { - check_single_ai_agent(info, found, &cmds, main.as_ref()) - }) - }) - .collect(); - - let (c_git, c_gh, c_gh_auth, c_git_lfs, c_clonefile) = - tokio::join!(c_git, c_gh, c_gh_auth, c_git_lfs, c_clonefile); - - let mut checks = vec![ - c_git.unwrap_or_else(|_| empty_check("git", "Git")), - c_gh.unwrap_or_else(|_| empty_check("gh", "GitHub CLI")), - c_gh_auth.unwrap_or_else(|_| empty_check("gh-auth", "GitHub Auth")), - c_git_lfs.unwrap_or_else(|_| empty_check("git-lfs", "Git LFS")), - c_clonefile.unwrap_or_else(|_| empty_check("git-clonefile", "Copy on Write Git Clones")), - ]; - - for (i, handle) in agent_handles.into_iter().enumerate() { - let info = &AI_AGENT_CHECKS[i]; - checks.push( - handle - .await - .unwrap_or_else(|_| empty_check(info.id, info.label)), - ); - } - - DoctorReport { checks } -} - -/// Run a fix command for a doctor check, identified by check ID and fix type. -/// -/// The actual shell command is looked up from the static check definitions — -/// the frontend never sends a raw command string. -#[tauri::command] -pub async fn run_doctor_fix(check_id: String, fix_type: FixType) -> Result<(), String> { - let command = lookup_fix_command(&check_id, &fix_type) - .ok_or_else(|| format!("Unknown check '{check_id}' or fix type '{fix_type:?}'"))?; - - tokio::task::spawn_blocking(move || { - let (shell, args) = if std::path::Path::new("/bin/zsh").exists() { - ("/bin/zsh", vec!["-l", "-c", &command]) - } else { - ("/bin/bash", vec!["-l", "-c", &command]) - }; - let home = std::env::var("HOME").unwrap_or_else(|_| "/".to_string()); - let user = std::env::var("USER").unwrap_or_default(); - let output = Command::new(shell) - .args(&args) - .env_clear() - .env("HOME", &home) - .env("USER", &user) - .env("TERM", "xterm-256color") - .current_dir(&home) - .output() - .map_err(|e| format!("Failed to run command: {e}"))?; - - if output.status.success() { - Ok(()) - } else { - let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); - Err(if stderr.is_empty() { - format!("Command failed with exit code {}", output.status) - } else { - stderr - }) - } - }) - .await - .unwrap_or_else(|e| Err(format!("Task failed: {e}"))) -} diff --git a/ui/goose2/src-tauri/src/commands/doctor/resolve.rs b/ui/goose2/src-tauri/src/commands/doctor/resolve.rs deleted file mode 100644 index 2418c6ae61..0000000000 --- a/ui/goose2/src-tauri/src/commands/doctor/resolve.rs +++ /dev/null @@ -1,74 +0,0 @@ -//! Binary resolution and command output formatting helpers. - -use std::path::PathBuf; -use std::process::Command; - -use super::types::ResolvedBinary; - -/// Resolve a binary by trying login shell `which` then common install paths. -pub(super) fn resolve_binary(cmd: &str) -> ResolvedBinary { - let mut lines = vec![format!("resolve '{cmd}':")]; - - // Strategy 1: Login shell `which` (primary) - lines.push(" strategy 1 — login shell `which`:".to_string()); - for shell in &["/bin/zsh", "/bin/bash"] { - let which_cmd = format!("which {cmd}"); - match Command::new(shell).args(["-l", "-c", &which_cmd]).output() { - Ok(output) => { - let result = String::from_utf8_lossy(&output.stdout).trim().to_string(); - if output.status.success() && !result.is_empty() { - lines.push(format!( - " {shell} -l -c 'which {cmd}' => {result} (resolved)" - )); - return ResolvedBinary { - path: Some(PathBuf::from(&result)), - search_output: lines.join("\n"), - }; - } - lines.push(format!(" {shell} -l -c 'which {cmd}' => not found")); - } - Err(e) => { - lines.push(format!(" {shell} -l -c 'which {cmd}' => error: {e}")); - } - } - } - - // Strategy 2: Common install paths (fallback) - lines.push(" strategy 2 — common install paths (fallback):".to_string()); - for dir in &[ - "/opt/homebrew/bin", - "/usr/local/bin", - "/usr/bin", - "/home/linuxbrew/.linuxbrew/bin", - ] { - let path = PathBuf::from(dir).join(cmd); - if path.exists() { - lines.push(format!(" {} => found (resolved)", path.display())); - return ResolvedBinary { - path: Some(path), - search_output: lines.join("\n"), - }; - } - lines.push(format!(" {} => not found", path.display())); - } - - lines.push(" not found in any location".to_string()); - ResolvedBinary { - path: None, - search_output: lines.join("\n"), - } -} - -/// Format the raw output of a command invocation for debug diagnostics. -pub(super) fn format_command_output(cmd_desc: &str, output: &std::process::Output) -> String { - let stdout = String::from_utf8_lossy(&output.stdout); - let stderr = String::from_utf8_lossy(&output.stderr); - let mut raw = format!("$ {cmd_desc}\nexit code: {}", output.status); - if !stdout.trim().is_empty() { - raw.push_str(&format!("\nstdout:\n{}", stdout.trim())); - } - if !stderr.trim().is_empty() { - raw.push_str(&format!("\nstderr:\n{}", stderr.trim())); - } - raw -} diff --git a/ui/goose2/src-tauri/src/commands/doctor/types.rs b/ui/goose2/src-tauri/src/commands/doctor/types.rs deleted file mode 100644 index 7863ddc59f..0000000000 --- a/ui/goose2/src-tauri/src/commands/doctor/types.rs +++ /dev/null @@ -1,65 +0,0 @@ -//! Types shared across the doctor sub-modules. - -use serde::{Deserialize, Serialize}; -use std::path::PathBuf; - -/// Severity level for a single check. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "lowercase")] -pub enum CheckStatus { - Pass, - Warn, - Fail, -} - -/// The type of fix available for a check. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "lowercase")] -pub enum FixType { - /// A shell command to install or configure the dependency. - Command, - /// A shell command to install the ACP bridge binary. - Bridge, -} - -/// A single health-check result shown in the UI. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DoctorCheck { - /// Short identifier, e.g. "git" - pub id: String, - /// Human-readable label, e.g. "Git" - pub label: String, - pub status: CheckStatus, - /// One-line explanation shown next to the status badge. - pub message: String, - /// If non-None, the UI shows an "Install" link that opens this URL. - pub fix_url: Option, - /// If non-None, the UI shows the command text in a confirmation dialog. - /// Display-only — never sent back to the backend for execution. - pub fix_command: Option, - /// The type of fix available for this check. - /// Sent back to the backend along with the check ID to execute the fix. - pub fix_type: Option, - /// If non-None, the resolved path to the main executable on disk. - pub path: Option, - /// If non-None, the resolved path to the ACP bridge executable on disk. - pub bridge_path: Option, - /// Raw debug output: command stdout/stderr, search paths tried, etc. - /// Used by the "Copy details" feature for support diagnostics. - pub raw_output: Option, -} - -/// The full report returned to the frontend. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct DoctorReport { - pub checks: Vec, -} - -/// A resolved binary: the path (if found) and the diagnostic search trace. -#[derive(Clone)] -pub(super) struct ResolvedBinary { - pub path: Option, - pub search_output: String, -}