use std::collections::BTreeSet;
use std::io::Write;
use std::path::{Path, PathBuf};
use anyhow::Result;
use clap::Args;
use crate::Exit;
use crate::cli::MachineFiles;
use crate::files;
use crate::languages;
mod llm;
mod site_section;
const HEADER_RULE: &str = "============================================================";
#[derive(Debug, Args)]
pub struct DoctorArgs {
#[arg(value_name = "PATH", default_value = ".")]
pub path: PathBuf,
#[arg(long, value_name = "FILE")]
pub config: Option<PathBuf>,
}
pub async fn run(args: &DoctorArgs) -> Result<Exit> {
let mut out = std::io::stdout().lock();
match run_to(&mut out, args).await {
Ok(exit) => Ok(exit),
Err(err) if is_broken_pipe(&err) => Ok(Exit::Clean),
Err(err) => Err(err),
}
}
pub(crate) fn is_broken_pipe(err: &anyhow::Error) -> bool {
err.downcast_ref::<std::io::Error>()
.is_some_and(|io| io.kind() == std::io::ErrorKind::BrokenPipe)
}
pub async fn run_to<W: Write>(out: &mut W, args: &DoctorArgs) -> Result<Exit> {
run_at(
out,
args,
&MachineFiles {
auth: &crate::auth::default_path()?,
policy: &crate::config::site::default_path(),
},
)
.await
}
pub async fn run_at<W: Write>(
out: &mut W,
args: &DoctorArgs,
machine: &MachineFiles<'_>,
) -> Result<Exit> {
run_at_with_codex(out, args, machine, &crate::llm::codex::current_status).await
}
pub(crate) async fn run_at_with_codex<W: Write>(
out: &mut W,
args: &DoctorArgs,
machine: &MachineFiles<'_>,
codex_probe: &dyn Fn() -> Result<crate::llm::codex::CodexStatus, String>,
) -> Result<Exit> {
let root = args
.path
.canonicalize()
.unwrap_or_else(|_| args.path.clone());
writeln!(out, "drep in {}", root.display())?;
writeln!(out, "{HEADER_RULE}")?;
let files = files::expand_paths(std::slice::from_ref(&root), files::is_scan_target);
let file_refs: Vec<&Path> = files.iter().map(PathBuf::as_path).collect();
let buckets = languages::group_by_language(&file_refs);
if buckets.is_empty() {
writeln!(out)?;
writeln!(out, "No source files drep recognises were found here.")?;
write_configuration(out, args, &root, &files, machine, codex_probe).await?;
return Ok(Exit::Clean);
}
write_languages_section(out, &buckets)?;
let missing = write_tools_section(out, &buckets, &root)?;
write_configuration(out, args, &root, &files, machine, codex_probe).await?;
if let Some(line) = missing_tools_line(&missing) {
writeln!(out)?;
writeln!(out, "{line}")?;
}
Ok(Exit::Clean)
}
async fn write_configuration<W: Write>(
out: &mut W,
args: &DoctorArgs,
root: &Path,
source_files: &[PathBuf],
machine: &MachineFiles<'_>,
codex_probe: &dyn Fn() -> Result<crate::llm::codex::CodexStatus, String>,
) -> Result<()> {
let site = crate::config::site::load(machine.policy);
let in_effect = site.as_ref().ok().and_then(Option::as_ref);
let refusal = match in_effect {
Some(site) => {
let directories = doctor_policy_directories(root, source_files);
site.refusal_among(&directories, machine.policy).await
}
None => Ok(None),
};
site_section::write_site_section(out, machine.policy, &site, &refusal)?;
llm::write_llm_section(
out,
args,
root,
machine.auth,
in_effect,
Semantic::of(&site, &refusal),
codex_probe,
)
.await
}
fn doctor_policy_directories(root: &Path, source_files: &[PathBuf]) -> BTreeSet<PathBuf> {
if source_files.is_empty() {
return BTreeSet::from([root.to_path_buf()]);
}
source_files
.iter()
.filter_map(|file| file.parent().map(Path::to_path_buf))
.collect()
}
#[derive(Clone, Copy)]
pub(super) enum Semantic {
Permitted,
Refused,
Unevaluable,
}
impl Semantic {
pub(super) fn skip_reason(self) -> Option<&'static str> {
match self {
Self::Permitted => None,
Self::Refused => {
Some("not attempted, because site policy refuses semantic review here")
}
Self::Unevaluable => {
Some("not attempted, because the site policy above could not be evaluated")
}
}
}
fn of(
site: &Result<
Option<crate::config::site::SiteConfig>,
crate::config::site::SiteConfigError,
>,
refusal: &Result<
Option<crate::config::site::Refusal>,
crate::config::site::SiteConfigError,
>,
) -> Self {
match (site, refusal) {
(Err(_), _) | (_, Err(_)) => Self::Unevaluable,
(Ok(_), Ok(Some(_))) => Self::Refused,
(Ok(_), Ok(None)) => Self::Permitted,
}
}
}
fn missing_tools_line(missing: &[&str]) -> Option<String> {
if missing.is_empty() {
return None;
}
Some(format!(
"{} configured tool(s) are missing: {}. drep exits 2 rather than reporting those files clean.",
missing.len(),
missing.join(", "),
))
}
fn write_languages_section<W: Write>(
out: &mut W,
buckets: &[(&'static languages::spec::LanguageSupport, Vec<&Path>)],
) -> Result<()> {
writeln!(out)?;
writeln!(out, "Languages found:")?;
for (language, paths) in buckets {
writeln!(out, " {}: {} file(s)", language.display_name, paths.len())?;
}
Ok(())
}
fn write_tools_section<W: Write>(
out: &mut W,
buckets: &[(&'static languages::spec::LanguageSupport, Vec<&Path>)],
root: &Path,
) -> Result<Vec<&'static str>> {
writeln!(out)?;
writeln!(out, "Deterministic checks (these gate):")?;
let mut missing: Vec<&'static str> = Vec::new();
for (language, paths) in buckets {
if language.tools.is_empty() {
writeln!(out, " {}: no tools wired up yet", language.display_name)?;
continue;
}
for spec in language.tools {
let roots: BTreeSet<PathBuf> = paths
.iter()
.filter_map(|path| languages::runner::configuration_root(spec, root, path))
.collect();
let outcome = if roots.is_empty() {
languages::runner::tool_status(spec, root)
} else {
workspace_tool_status(spec, root, &roots)
};
writeln!(out, " {}: {}", spec.name, outcome.detail)?;
if matches!(outcome.status, languages::runner::ToolStatus::Unavailable)
&& !missing.contains(&spec.name)
{
missing.push(spec.name);
}
}
}
Ok(missing)
}
fn workspace_tool_status(
spec: &'static languages::spec::ToolSpec,
root: &Path,
roots: &BTreeSet<PathBuf>,
) -> languages::runner::ToolOutcome {
let statuses: Vec<_> = roots
.iter()
.map(|workspace| languages::runner::tool_status_at(spec, root, workspace))
.collect();
if let Some(unavailable) = statuses
.iter()
.find(|outcome| matches!(outcome.status, languages::runner::ToolStatus::Unavailable))
{
return unavailable.clone();
}
let detail = if roots.len() == 1 && roots.contains(&root.to_path_buf()) {
"ready".to_owned()
} else {
format!("ready in {} workspace(s)", roots.len())
};
languages::runner::ToolOutcome {
tool: spec.name,
status: languages::runner::ToolStatus::Ok,
findings: Vec::new(),
detail,
compilation_succeeded: false,
}
}
#[cfg(test)]
mod unit_tests;
#[cfg(test)]
mod tests;