mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
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) <noreply@anthropic.com> * fix: remove unused doctor imports flagged by clippy Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Generated
+65
-62
@@ -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",
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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<String> {
|
||||
// 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
|
||||
}
|
||||
@@ -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}"
|
||||
)),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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<ResolvedBinary> = 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}")))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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<String>,
|
||||
/// 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<String>,
|
||||
/// 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<FixType>,
|
||||
/// If non-None, the resolved path to the main executable on disk.
|
||||
pub path: Option<String>,
|
||||
/// If non-None, the resolved path to the ACP bridge executable on disk.
|
||||
pub bridge_path: Option<String>,
|
||||
/// Raw debug output: command stdout/stderr, search paths tried, etc.
|
||||
/// Used by the "Copy details" feature for support diagnostics.
|
||||
pub raw_output: Option<String>,
|
||||
}
|
||||
|
||||
/// The full report returned to the frontend.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DoctorReport {
|
||||
pub checks: Vec<DoctorCheck>,
|
||||
}
|
||||
|
||||
/// A resolved binary: the path (if found) and the diagnostic search trace.
|
||||
#[derive(Clone)]
|
||||
pub(super) struct ResolvedBinary {
|
||||
pub path: Option<PathBuf>,
|
||||
pub search_output: String,
|
||||
}
|
||||
Reference in New Issue
Block a user