From 1fafbf3b2c568190b49b0f63ece2e853f829bea2 Mon Sep 17 00:00:00 2001 From: Nishant Arora <1895906+whizzzkid@users.noreply.github.com> Date: Thu, 29 Jan 2026 20:02:39 +0000 Subject: [PATCH] feat: add public doctor API for programmatic usage - Add src/doctor/api.rs with run() and list() functions - Update src/doctor/check.rs for API integration - Update src/doctor/commands/run.rs to use new runner - Update src/doctor/mod.rs with new exports - Refactor src/doctor/runner.rs for library usage Co-Authored-By: Claude (global.anthropic.claude-opus-4-5-20251101-v1:0) --- src/doctor/api.rs | 213 +++++++++++++++++++++++++++++++++++++ src/doctor/check.rs | 5 +- src/doctor/commands/run.rs | 12 ++- src/doctor/mod.rs | 9 ++ src/doctor/runner.rs | 41 +++++-- 5 files changed, 263 insertions(+), 17 deletions(-) create mode 100644 src/doctor/api.rs diff --git a/src/doctor/api.rs b/src/doctor/api.rs new file mode 100644 index 0000000..fb6bfab --- /dev/null +++ b/src/doctor/api.rs @@ -0,0 +1,213 @@ +//! Public API for the doctor module. +//! +//! This module provides the main library entry points for programmatic usage +//! of the doctor functionality without CLI dependencies. +//! +//! # Examples +//! +//! ## Run All Checks with Auto-Fix +//! +//! ```rust,no_run +//! use dx_scope::{DoctorRunOptions, FoundConfig}; +//! use dx_scope::doctor::run; +//! +//! #[tokio::main] +//! async fn main() -> anyhow::Result<()> { +//! // Load configuration +//! let working_dir = std::env::current_dir()?; +//! let config = FoundConfig::empty(working_dir); +//! +//! // Configure options with auto-fix enabled +//! let options = DoctorRunOptions::with_fixes(); +//! +//! let result = run(&config, options).await?; +//! +//! println!("Success: {}", result.did_succeed); +//! println!("Passed: {}", result.succeeded_groups.len()); +//! println!("Failed: {}", result.failed_group.len()); +//! +//! Ok(()) +//! } +//! ``` + +use crate::doctor::check::{DefaultDoctorActionRun, DefaultGlobWalker}; +use crate::doctor::file_cache::{FileBasedCache, FileCache, NoOpCache}; +use crate::doctor::options::DoctorRunOptions; +use crate::doctor::runner::{GroupActionContainer, PathRunResult, RunGroups, compute_group_order}; +use crate::shared::directories; +use crate::shared::prelude::{DefaultExecutionProvider, ExecutionProvider, FoundConfig}; +use anyhow::Result; +use std::collections::{BTreeMap, BTreeSet}; +use std::sync::Arc; +use tracing::{info, warn}; + +/// Run doctor health checks. +/// +/// This is the main library entry point for running doctor checks programmatically. +/// It runs health checks defined in the configuration and optionally applies fixes. +/// +/// # Arguments +/// +/// * `config` - Loaded scope configuration +/// * `options` - Doctor run options (groups, fix mode, cache settings) +/// +/// # Returns +/// +/// Returns `PathRunResult` containing: +/// - `did_succeed`: Overall success/failure +/// - `succeeded_groups`: Names of groups that passed +/// - `failed_group`: Names of groups that failed +/// - `skipped_group`: Names of groups that were skipped +/// - `group_reports`: Detailed reports for each group +/// +/// # Examples +/// +/// ```rust +/// use dx_scope::{DoctorRunOptions, FoundConfig}; +/// use dx_scope::doctor::run; +/// +/// let working_dir = std::env::current_dir().unwrap(); +/// let config = FoundConfig::empty(working_dir); +/// let options = DoctorRunOptions::with_fixes(); +/// // Call: run(&config, options).await +/// ``` +/// +/// # Note on Interaction +/// +/// When `options.run_fix` is true, fixes will be applied automatically without prompting. +/// For non-interactive/CI environments, this is the recommended mode. +pub async fn run( + config: &FoundConfig, + options: DoctorRunOptions, +) -> Result +{ + info!("Starting doctor run"); + + // Get cache implementation + let file_cache: Arc = if options.no_cache { + Arc::::default() + } else { + let cache_dir_path = options.cache_dir.clone().unwrap_or_else(|| { + directories::cache() + .expect("Unable to determine cache directory") + .join("scope") + }); + + let cache_path = cache_dir_path.join("cache-file.json"); + + match FileBasedCache::new(&cache_path) { + Ok(cache) => Arc::new(cache), + Err(e) => { + warn!("Unable to create cache {:?}", e); + Arc::::default() + } + } + }; + + // Get execution provider + let exec_runner: Arc = Arc::new(DefaultExecutionProvider::default()); + let glob_walker = Arc::new(DefaultGlobWalker::default()); + + // Build group containers and desired groups set + let mut groups = BTreeMap::new(); + let mut desired_groups = BTreeSet::new(); + let run_fix = options.run_fix; + + for group in config.doctor_group.values() { + let should_group_run = match &options.only_groups { + None => group.run_by_default, + Some(names) => names.contains(&group.metadata.name().to_string()), + }; + + let mut action_runs = Vec::new(); + + for action in &group.actions { + let run = DefaultDoctorActionRun { + model: group.clone(), + action: action.clone(), + working_dir: config.working_dir.clone(), + file_cache: file_cache.clone(), + run_fix, + exec_runner: exec_runner.clone(), + glob_walker: glob_walker.clone(), + known_errors: config.known_error.clone(), + }; + + action_runs.push(run); + } + + let container = GroupActionContainer::new( + group.clone(), + action_runs, + exec_runner.clone(), + config.working_dir.clone(), + config.bin_path.clone(), + ); + + let group_name = container.group_name().to_string(); + groups.insert(group_name.clone(), container); + + if should_group_run { + desired_groups.insert(group_name); + } + } + + // Compute group order + let all_paths = compute_group_order(&config.doctor_group, desired_groups); + + if all_paths.is_empty() { + warn!("Could not find any tasks to execute"); + } + + // Execute groups + let run_groups = RunGroups { + group_actions: groups, + all_paths, + yolo: options.run_fix, // Auto-approve fixes when run_fix is enabled + }; + let result = run_groups.execute().await?; + + // Persist cache + if let Err(e) = file_cache.persist().await { + info!("Unable to store cache {:?}", e); + warn!("Unable to update cache, re-runs may redo work"); + } + + info!( + "Doctor run completed: {} succeeded, {} failed, {} skipped", + result.succeeded_groups.len(), + result.failed_group.len(), + result.skipped_group.len() + ); + + Ok(result) +} + +/// List available doctor checks. +/// +/// Returns information about all available doctor checks and groups +/// defined in the configuration. +/// +/// # Arguments +/// +/// * `config` - Loaded scope configuration +/// +/// # Returns +/// +/// Returns a vector of doctor groups with their checks. +/// +/// # Examples +/// +/// ```rust +/// use dx_scope::FoundConfig; +/// use dx_scope::doctor::list; +/// +/// let working_dir = std::env::current_dir().unwrap(); +/// let config = FoundConfig::empty(working_dir); +/// // Call: list(&config).await +/// // Then iterate: for group in groups { ... } +/// ``` +pub async fn list(config: &FoundConfig) -> Result> { + let order = super::commands::generate_doctor_list(config); + Ok(order.clone()) +} diff --git a/src/doctor/check.rs b/src/doctor/check.rs index c4ca290..175e6a9 100644 --- a/src/doctor/check.rs +++ b/src/doctor/check.rs @@ -6,6 +6,7 @@ use std::{cmp, vec}; use tokio::io::BufReader; use tracing::debug; +use crate::internal::prompts::AutoApprove; use crate::models::HelpMetadata; use crate::prelude::{ ActionReport, ActionReportBuilder, ActionTaskReport, DoctorCommand, KnownError, @@ -672,7 +673,9 @@ impl DefaultDoctorActionRun { .collect::>(); let buffer = BufReader::new(StringVecReader::new(&lines)); - analyze::process_lines(&self.known_errors, &self.working_dir, buffer).await + // Use AutoApprove for self-healing: known errors during doctor runs + // are automatically fixed without user prompts + analyze::process_lines(&self.known_errors, &self.working_dir, buffer, &AutoApprove).await } } diff --git a/src/doctor/commands/run.rs b/src/doctor/commands/run.rs index 7fc75fd..5971403 100644 --- a/src/doctor/commands/run.rs +++ b/src/doctor/commands/run.rs @@ -54,12 +54,14 @@ fn get_cache(args: &DoctorRunArgs) -> Arc { let old_default_cache_path = PathBuf::from("/tmp/scope/cache-file.json"); // Handle backward compatibility: migrate from old location to new location - if cache_dir != "/tmp/scope" + let should_migrate = cache_dir != "/tmp/scope" && old_default_cache_path.exists() - && !cache_path.exists() - && let Err(e) = migrate_old_cache(&old_default_cache_path, &cache_path) - { - warn!("Unable to migrate cache from old location: {:?}", e); + && !cache_path.exists(); + + if should_migrate { + if let Err(e) = migrate_old_cache(&old_default_cache_path, &cache_path) { + warn!("Unable to migrate cache from old location: {:?}", e); + } } match FileBasedCache::new(&cache_path) { diff --git a/src/doctor/mod.rs b/src/doctor/mod.rs index 7516fb0..47346b8 100644 --- a/src/doctor/mod.rs +++ b/src/doctor/mod.rs @@ -1,8 +1,10 @@ +mod api; mod check; mod cli; mod commands; mod error; mod file_cache; +pub mod options; mod runner; #[cfg(test)] mod tests; @@ -12,3 +14,10 @@ pub mod prelude { pub use super::cli::doctor_root; pub use super::commands::generate_doctor_list; } + +// Re-export key types for library usage +pub use options::DoctorRunOptions; +pub use runner::PathRunResult; + +// Public API functions +pub use api::{run, list}; diff --git a/src/doctor/runner.rs b/src/doctor/runner.rs index 9c82d70..2b72732 100644 --- a/src/doctor/runner.rs +++ b/src/doctor/runner.rs @@ -1,12 +1,12 @@ use super::check::{ActionRunResult, ActionRunStatus, DoctorActionRun}; use crate::doctor::check::RuntimeError; +use crate::internal::prompts::UserInteraction; use crate::models::HelpMetadata; -use crate::prelude::{ - ActionReport, ActionTaskReport, CaptureOpts, ExecutionProvider, GroupReport, OutputDestination, - SkipSpec, generate_env_vars, progress_bar_without_pos, +use crate::models::prelude::SkipSpec; +use crate::shared::prelude::{ + ActionReport, ActionTaskReport, CaptureOpts, DoctorGroup, ExecutionProvider, GroupReport, + OutputDestination, generate_env_vars, progress_bar_without_pos, }; -use crate::report_stdout; -use crate::shared::prelude::DoctorGroup; use anyhow::Result; use colored::Colorize; use opentelemetry::trace::Status; @@ -350,6 +350,10 @@ where } } +/// Prompt the user for confirmation using the inquire crate. +/// +/// This function wraps inquire::Confirm and handles TTY detection. +/// It's used when `yolo` mode is disabled. fn prompt_user(prompt_text: &str, maybe_help_text: &Option) -> bool { tracing_indicatif::suspend_tracing_indicatif(|| { let prompt = { @@ -364,14 +368,31 @@ fn prompt_user(prompt_text: &str, maybe_help_text: &Option) -> bool { }) } +/// Auto-approve all prompts. +/// +/// This function automatically approves all prompts, used in `yolo` mode +/// or when running non-interactively. fn auto_approve(prompt_text: &str, maybe_help_text: &Option) -> bool { - println!("{} Yes (auto-approved)", prompt_text); + info!(target: "progress", prompt = %prompt_text, "Auto-approved"); if let Some(help_text) = maybe_help_text { - println!("[{}]", help_text); + debug!(target: "progress", help = %help_text, "Auto-approve help text"); } true } +/// Create a prompt function from a UserInteraction implementation. +/// +/// This bridges the gap between the trait-based UserInteraction interface +/// and the function pointer interface used by DoctorActionRun. +#[allow(dead_code)] +pub fn make_prompt_fn(user_interaction: &U) -> impl Fn(&str, &Option) -> bool + '_ { + move |prompt_text: &str, maybe_help_text: &Option| { + tracing_indicatif::suspend_tracing_indicatif(|| { + user_interaction.confirm(prompt_text, maybe_help_text.as_deref()) + }) + } +} + async fn report_action_output( group_name: &str, action: &T, @@ -445,10 +466,8 @@ async fn print_pretty_result( let task_reports = action_task_reports_for_display(&result.action_report); for task in task_reports { if let Some(text) = task.output { - let line_prefix = format!("{group_name}/{action_name}"); for line in text.lines() { - let output_line = format!("{}: {}", line_prefix.dimmed(), line); - report_stdout!("{}", output_line); + error!(target: "user", group = group_name, action = action_name, "{}", line); } } } @@ -527,7 +546,7 @@ mod tests { }; use crate::doctor::runner::{GroupActionContainer, RunGroups, compute_group_order}; use crate::doctor::tests::{group_noop, make_root_model_additional}; - use crate::prelude::{ActionReport, ActionTaskReport, MockExecutionProvider}; + use crate::shared::prelude::{ActionReport, ActionTaskReport, MockExecutionProvider}; use anyhow::Result; use std::collections::{BTreeMap, BTreeSet}; use std::sync::Arc;