use anyhow::{bail, Result};
use console::style;
use crate::config::Config;
use crate::remotes::render_remote_health;
use crate::targets::{render_target_health, CommandRunner, HealthStatus};
use crate::ui;
use super::types::{Check, CheckStatus, Report, Section};
impl Report {
pub(crate) fn render(&self, runner: &dyn CommandRunner) -> Result<()> {
let project_label = self.project.as_deref().unwrap_or("unknown project");
cliclack::intro(
style(format!("esk doctor ยท {project_label}"))
.bold()
.to_string(),
)?;
let term = console::Term::stderr();
let bar = style("\u{2502}").dim();
render_checked_section(&term, &bar, "Project structure", &self.structure)?;
let config = self.project.as_ref().and_then(|_| {
let config_path = self.root.join("esk.yaml");
Config::load(&config_path).ok()
});
render_section(&term, &bar, "Config", &self.config)?;
let mut target_ok = 0usize;
let mut target_fail = 0usize;
let mut remote_ok = 0usize;
let mut remote_fail = 0usize;
if let Some(ref cfg) = config {
if !cfg.typed_targets.is_empty() {
let health = render_target_health(cfg, runner, "Targets");
for h in &health {
if h.status.is_ok() {
target_ok += 1;
} else {
target_fail += 1;
}
}
}
if !cfg.typed_remotes.is_empty() {
let health = render_remote_health(cfg, runner, "Remotes");
for h in &health {
match &h.status {
HealthStatus::Ok(_) => remote_ok += 1,
HealthStatus::Failed(_) => remote_fail += 1,
}
}
}
}
render_section(&term, &bar, "Store consistency", &self.store_consistency)?;
render_section(&term, &bar, "Secrets", &self.secrets_health)?;
if !self.suggestions.is_empty() {
let cmd_width = self
.suggestions
.iter()
.map(|s| s.command.len())
.max()
.unwrap_or(0);
term.write_line(&format!("{} Suggestions", style("\u{25C7}").dim()))?;
for s in &self.suggestions {
term.write_line(&format!(
"{bar} {} {}",
style(format!("{:<width$}", s.command, width = cmd_width)).cyan(),
style(&s.reason).dim()
))?;
}
term.write_line(&format!("{bar}"))?;
}
let (mut pass, mut warn, mut fail) = count_checks(&self.structure);
count_section(&self.config, &mut pass, &mut warn, &mut fail);
count_section(&self.store_consistency, &mut pass, &mut warn, &mut fail);
count_section(&self.secrets_health, &mut pass, &mut warn, &mut fail);
pass += target_ok + remote_ok;
fail += target_fail + remote_fail;
let summary = format!("{pass} passed, {warn} warnings, {fail} failures");
if fail > 0 {
cliclack::outro(style(&summary).red().bold().to_string())?;
bail!("{summary}");
}
let outro_style = if warn > 0 {
style(&summary).yellow()
} else {
style(&summary).green()
};
cliclack::outro(outro_style.to_string())?;
Ok(())
}
}
fn render_checked_section(
term: &console::Term,
bar: &console::StyledObject<&str>,
title: &str,
checks: &[Check],
) -> std::io::Result<()> {
let header_icon = section_icon(checks);
let label_width = checks.iter().map(|c| c.label.len()).max().unwrap_or(0) + 2;
term.write_line(&format!("{header_icon} {title}"))?;
for c in checks {
let icon = check_icon(c.status);
term.write_line(&format!(
"{bar} {} {:<label_width$}{}",
icon,
c.label,
style(&c.detail).dim(),
))?;
}
term.write_line(&format!("{bar}"))?;
Ok(())
}
fn render_section(
term: &console::Term,
bar: &console::StyledObject<&str>,
title: &str,
section: &Section,
) -> std::io::Result<()> {
match section {
Section::Checked(checks) => render_checked_section(term, bar, title, checks),
Section::Skipped(reason) => {
term.write_line(&format!("{} {title}", style("\u{25C7}").dim()))?;
term.write_line(&format!(
"{bar} {} {}",
style("\u{2014}").dim(),
style(format!("skipped: {reason}")).dim()
))?;
term.write_line(&format!("{bar}"))?;
Ok(())
}
}
}
fn section_icon(checks: &[Check]) -> console::StyledObject<&'static str> {
let all_pass = checks.is_empty() || checks.iter().all(|c| c.status == CheckStatus::Pass);
let all_fail = !checks.is_empty() && checks.iter().all(|c| c.status == CheckStatus::Fail);
if all_pass {
style("\u{25C6}").green()
} else if all_fail {
style("\u{25C6}").red()
} else {
style("\u{25C6}").yellow()
}
}
fn check_icon(status: CheckStatus) -> ui::Icon {
match status {
CheckStatus::Pass => ui::Icon::Success,
CheckStatus::Warn => ui::Icon::Warning,
CheckStatus::Fail => ui::Icon::Failure,
}
}
fn count_checks(checks: &[super::types::Check]) -> (usize, usize, usize) {
let mut pass = 0;
let mut warn = 0;
let mut fail = 0;
for c in checks {
match c.status {
CheckStatus::Pass => pass += 1,
CheckStatus::Warn => warn += 1,
CheckStatus::Fail => fail += 1,
}
}
(pass, warn, fail)
}
fn count_section(section: &Section, pass: &mut usize, warn: &mut usize, fail: &mut usize) {
if let Section::Checked(checks) = section {
let (p, w, f) = count_checks(checks);
*pass += p;
*warn += w;
*fail += f;
}
}