use colored::Colorize;
use crate::display::colors::stripped_len;
use crate::display::icons;
pub struct PhaseResult {
pub name: &'static str,
pub passed: bool,
pub detail: String,
pub duration_ms: u64,
pub errors: Vec<String>,
pub hints: Vec<String>,
}
pub struct McpCheckResult {
pub server_name: String,
pub tool_count: usize,
pub connect_ms: u64,
pub validations: Vec<McpCallValidation>,
}
pub struct McpCallValidation {
pub task_id: String,
pub tool_name: String,
pub valid: bool,
pub errors: Vec<McpParamError>,
}
pub struct McpParamError {
pub path: String,
pub message: String,
}
fn term_width() -> usize {
terminal_size::terminal_size()
.map(|(tw, _)| tw.0 as usize)
.unwrap_or(80)
.min(72)
}
pub fn print_check_header(file: &str, strict: bool, version: &str) {
let w = term_width();
let inner = w - 2;
let border = "\u{2500}".repeat(inner);
println!("\u{256D}{}\u{256E}", border.dimmed());
println!("\u{2502}{}\u{2502}", " ".repeat(inner));
let title = if strict {
"N I K A C H E C K \u{2500} \u{2500} S T R I C T"
} else {
"N I K A C H E C K"
};
let ver = format!("v{}", version);
let pad = inner.saturating_sub(title.len() + ver.len() + 4);
println!(
"\u{2502} {}{}{} \u{2502}",
title.bold().white(),
" ".repeat(pad),
ver.dimmed()
);
println!("\u{2502}{}\u{2502}", " ".repeat(inner));
let file_pad = inner.saturating_sub(file.len() + 2);
println!("\u{2502} {}{}\u{2502}", file.bold(), " ".repeat(file_pad));
println!("\u{2502}{}\u{2502}", " ".repeat(inner));
println!("\u{2570}{}\u{256F}", border.dimmed());
println!();
}
pub fn print_phase(result: &PhaseResult) {
let icon = if result.passed {
icons::success()
} else {
icons::failed()
};
let dur = format!("{}ms", result.duration_ms);
let name_padded = format!("{:<16}", result.name);
let detail_padded = format!("{:<50}", result.detail);
println!(
" {} {} {} {}",
icon,
name_padded,
detail_padded,
dur.dimmed()
);
for err in &result.errors {
println!(" {}", "\u{2502}".dimmed());
println!(" {} {}", "\u{2502}".dimmed(), err.red());
}
if !result.hints.is_empty() {
println!(" {}", "\u{2502}".dimmed());
let max_w = result.hints.iter().map(|h| h.len()).max().unwrap_or(40);
let dashes = "\u{254C}".repeat(max_w + 2);
println!(
" {} \u{256D}{}\u{256E}",
"\u{2502}".dimmed(),
dashes.dimmed()
);
for hint in &result.hints {
let pad = max_w.saturating_sub(hint.len());
println!(
" {} \u{2502} {}{} \u{2502}",
"\u{2502}".dimmed(),
hint,
" ".repeat(pad)
);
}
println!(
" {} \u{2570}{}\u{256F}",
"\u{2502}".dimmed(),
dashes.dimmed()
);
}
}
pub fn print_phase_skipped(name: &str, reason: &str) {
let name_padded = format!("{:<16}", name);
println!(
" {} {} {}",
icons::skipped(),
name_padded,
format!("skipped ({})", reason).dimmed()
);
}
pub fn print_mcp_validation(results: &[McpCheckResult]) {
let w = term_width();
println!();
let label = "\u{2500}\u{2500} MCP Validation ";
let fill = "\u{2500}".repeat(w.saturating_sub(label.len() + 2));
println!(" {}{}", label.dimmed(), fill.dimmed());
println!();
for result in results {
println!(" {} {}", icons::mcp(), result.server_name.green().bold());
let conn_info = format!("connected \u{00B7} {} tools available", result.tool_count);
let dur_str = format!("{}ms", result.connect_ms);
let conn_pad = w.saturating_sub(
4 + conn_info.len() + dur_str.len() + 2,
);
println!(
" {} {} \u{00B7} {} tools available{}{}",
"\u{2502}".dimmed(),
"connected".green(),
result.tool_count,
" ".repeat(conn_pad),
dur_str.dimmed()
);
println!(" {}", "\u{2502}".dimmed());
let mut valid_count = 0u32;
let total = result.validations.len() as u32;
for v in &result.validations {
if v.valid {
valid_count += 1;
println!(
" {} {} {:<14}\u{2192} {:<24} {}",
"\u{2502}".dimmed(),
icons::success(),
v.task_id,
v.tool_name,
"params valid".dimmed()
);
} else {
println!(
" {} {} {:<14}\u{2192} {:<24} {}",
"\u{2502}".dimmed(),
icons::failed(),
v.task_id.red(),
v.tool_name,
format!("{} errors", v.errors.len()).red()
);
for err in &v.errors {
println!(
" {} {} {} {}",
"\u{2502}".dimmed(),
"\u{2502}".dimmed(),
format!("[{}]", err.path).yellow(),
err.message.dimmed()
);
}
}
}
println!(" {}", "\u{2502}".dimmed());
let summary = format!("{}/{} calls valid", valid_count, total);
let summary_colored = if valid_count == total {
summary.green()
} else {
summary.yellow()
};
println!(" {} {}", "\u{2502}".dimmed(), summary_colored);
println!();
}
}
#[allow(clippy::too_many_arguments)]
pub fn print_check_summary(
valid: bool,
total_ms: u64,
task_count: usize,
edge_count: usize,
layer_count: usize,
schema_count: u32,
strict_info: Option<(u32, u32, u32)>, error_codes: &[(&str, &str)], ) {
let w = term_width();
let inner = w - 2;
let border = "\u{2500}".repeat(inner);
println!("\u{256D}{}\u{256E}", border.dimmed());
println!("\u{2502}{}\u{2502}", " ".repeat(inner));
let (icon, label) = if valid {
(icons::success(), "V A L I D".green().bold())
} else {
(icons::failed(), "I N V A L I D".red().bold())
};
let dur = format!("{}ms", total_ms);
let status_line = format!(" {} {}", icon, label);
let pad = inner.saturating_sub(stripped_len(&status_line) + dur.len() + 2);
println!(
"\u{2502}{}{}{} \u{2502}",
status_line,
" ".repeat(pad),
dur.dimmed()
);
println!("\u{2502}{}\u{2502}", " ".repeat(inner));
let mut stats_parts = vec![
format!("{} tasks", task_count),
format!("{} edges", edge_count),
format!("{} layers", layer_count),
];
if schema_count > 0 {
stats_parts.push(format!("{} schemas", schema_count));
}
let stats = stats_parts.join(" \u{00B7} ");
let stats_pad = inner.saturating_sub(stats.len() + 2);
println!("\u{2502} {}{}\u{2502}", stats, " ".repeat(stats_pad));
if let Some((valid_calls, total_calls, param_errors)) = strict_info {
let strict_line = format!(
"strict: {}/{} MCP calls valid \u{00B7} {} param errors",
valid_calls, total_calls, param_errors
);
let strict_pad = inner.saturating_sub(strict_line.len() + 2);
println!(
"\u{2502} {}{}\u{2502}",
strict_line,
" ".repeat(strict_pad)
);
}
for (code, msg) in error_codes {
let err_line = format!("{}: {}", code, msg);
let err_pad = inner.saturating_sub(err_line.len() + 2);
println!(
"\u{2502} {}{}\u{2502}",
err_line.red(),
" ".repeat(err_pad)
);
}
println!("\u{2502}{}\u{2502}", " ".repeat(inner));
println!("\u{2570}{}\u{256F}", border.dimmed());
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_phase_result_pass() {
let result = PhaseResult {
name: "schema",
passed: true,
detail: "YAML valid against @0.12".to_string(),
duration_ms: 1,
errors: vec![],
hints: vec![],
};
print_phase(&result);
}
#[test]
fn test_phase_result_fail_with_hints() {
let result = PhaseResult {
name: "dag",
passed: false,
detail: "CYCLE DETECTED".to_string(),
duration_ms: 0,
errors: vec!["step_a \u{2192} step_b \u{2192} step_c \u{2192} step_a".to_string()],
hints: vec![
"Remove one dependency to break the cycle.".to_string(),
"Common fix: use with: binding instead of depends_on.".to_string(),
],
};
print_phase(&result);
}
#[test]
fn test_stripped_len_plain() {
assert_eq!(stripped_len("hello"), 5);
assert_eq!(stripped_len(""), 0);
assert_eq!(stripped_len("V A L I D"), 9);
}
#[test]
fn test_stripped_len_colored_crate() {
use colored::Colorize;
let green = "hello".green().to_string();
assert_eq!(stripped_len(&green), 5);
let bold_green = "\u{2713}".green().bold().to_string();
assert_eq!(stripped_len(&bold_green), 1);
}
}