1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
//! `cueloop doctor` command: handler.
//!
//! Purpose:
//! - `cueloop doctor` command: handler.
//!
//! Responsibilities:
//! - Provide focused implementation or regression coverage for this file's owning feature.
//!
//! Scope:
//! - Limited to this file's owning feature boundary.
//!
//!
//! Usage:
//! - Used through the crate module tree or integration test harness.
//!
//! Invariants/Assumptions:
//! - Keep behavior aligned with CueLoop's canonical CLI, machine-contract, and queue semantics.
use anyhow::Result;
use clap::{Args, ValueEnum};
use crate::sanity::{self, SanityOptions};
use crate::{commands::doctor, config};
/// Output format for doctor command.
#[derive(Debug, Clone, Copy, Default, ValueEnum)]
pub enum DoctorFormat {
/// Human-readable text output (default).
#[default]
Text,
/// Machine-readable JSON output for scripting/CI.
Json,
}
/// Arguments for the `cueloop doctor` command.
#[derive(Args)]
pub struct DoctorArgs {
/// Automatically fix all issues without prompting.
#[arg(long, conflicts_with = "no_sanity_checks")]
pub auto_fix: bool,
/// Skip sanity checks and only run doctor diagnostics.
#[arg(long, conflicts_with = "auto_fix")]
pub no_sanity_checks: bool,
/// Output format (text or json).
#[arg(long, value_enum, default_value = "text")]
pub format: DoctorFormat,
}
pub fn handle_doctor(args: DoctorArgs) -> Result<()> {
// Use resolve_from_cwd_for_doctor to skip instruction_files validation,
// allowing doctor to diagnose and warn about missing files without failing early.
let resolved = config::resolve_from_cwd_for_doctor()?;
// Run sanity checks first (unless skipped)
if !args.no_sanity_checks {
let options = SanityOptions {
auto_fix: args.auto_fix,
skip: false,
non_interactive: false, // doctor is always interactive by default
write_policy: sanity::SanityWritePolicy::AllowWrites,
};
let sanity_result = sanity::run_sanity_checks(&resolved, &options)?;
// Report sanity check results
if !sanity_result.auto_fixes.is_empty() {
log::info!(
"Sanity checks applied {} fix(es):",
sanity_result.auto_fixes.len()
);
for fix in &sanity_result.auto_fixes {
log::info!(" - {}", fix);
}
}
if !sanity_result.needs_attention.is_empty() {
log::warn!(
"Sanity checks found {} issue(s) needing attention:",
sanity_result.needs_attention.len()
);
for issue in &sanity_result.needs_attention {
match issue.severity {
sanity::IssueSeverity::Warning => log::warn!(" - {}", issue.message),
sanity::IssueSeverity::Error => log::error!(" - {}", issue.message),
}
}
}
}
// Run doctor checks with auto_fix flag and format
let report = doctor::run_doctor(&resolved, args.auto_fix)?;
// Output based on format
match args.format {
DoctorFormat::Text => {
doctor::print_doctor_report_text(&report);
}
DoctorFormat::Json => {
println!("{}", serde_json::to_string_pretty(&report)?);
}
}
// Return appropriate exit code based on report success
if report.success {
Ok(())
} else {
anyhow::bail!("Doctor check failed: one or more critical issues found")
}
}