1use colored::Colorize;
7
8use crate::display::colors::stripped_len;
9use crate::display::icons;
10
11pub struct PhaseResult {
13 pub name: &'static str,
14 pub passed: bool,
15 pub detail: String,
16 pub duration_ms: u64,
17 pub errors: Vec<String>,
19 pub hints: Vec<String>,
21}
22
23pub struct McpCheckResult {
25 pub server_name: String,
26 pub tool_count: usize,
27 pub connect_ms: u64,
28 pub validations: Vec<McpCallValidation>,
29}
30
31pub struct McpCallValidation {
33 pub task_id: String,
34 pub tool_name: String,
35 pub valid: bool,
36 pub errors: Vec<McpParamError>,
37}
38
39pub struct McpParamError {
41 pub path: String,
42 pub message: String,
43}
44
45fn term_width() -> usize {
47 terminal_size::terminal_size()
48 .map(|(tw, _)| tw.0 as usize)
49 .unwrap_or(80)
50 .min(72)
51}
52
53pub fn print_check_header(file: &str, strict: bool, version: &str) {
65 let w = term_width();
66 let inner = w - 2;
67 let border = "\u{2500}".repeat(inner);
68
69 println!("\u{256D}{}\u{256E}", border.dimmed());
70 println!("\u{2502}{}\u{2502}", " ".repeat(inner));
71
72 let title = if strict {
73 "N I K A C H E C K \u{2500} \u{2500} S T R I C T"
74 } else {
75 "N I K A C H E C K"
76 };
77 let ver = format!("v{}", version);
78 let pad = inner.saturating_sub(title.len() + ver.len() + 4);
79 println!(
80 "\u{2502} {}{}{} \u{2502}",
81 title.bold().white(),
82 " ".repeat(pad),
83 ver.dimmed()
84 );
85 println!("\u{2502}{}\u{2502}", " ".repeat(inner));
86
87 let file_pad = inner.saturating_sub(file.len() + 2);
89 println!("\u{2502} {}{}\u{2502}", file.bold(), " ".repeat(file_pad));
90
91 println!("\u{2502}{}\u{2502}", " ".repeat(inner));
92 println!("\u{2570}{}\u{256F}", border.dimmed());
93 println!();
94}
95
96pub fn print_phase(result: &PhaseResult) {
103 let icon = if result.passed {
104 icons::success()
105 } else {
106 icons::failed()
107 };
108
109 let dur = format!("{}ms", result.duration_ms);
110
111 let name_padded = format!("{:<16}", result.name);
113 let detail_padded = format!("{:<50}", result.detail);
114
115 println!(
116 " {} {} {} {}",
117 icon,
118 name_padded,
119 detail_padded,
120 dur.dimmed()
121 );
122
123 for err in &result.errors {
125 println!(" {}", "\u{2502}".dimmed());
126 println!(" {} {}", "\u{2502}".dimmed(), err.red());
127 }
128
129 if !result.hints.is_empty() {
131 println!(" {}", "\u{2502}".dimmed());
132 let max_w = result.hints.iter().map(|h| h.len()).max().unwrap_or(40);
133 let dashes = "\u{254C}".repeat(max_w + 2);
134 println!(
135 " {} \u{256D}{}\u{256E}",
136 "\u{2502}".dimmed(),
137 dashes.dimmed()
138 );
139 for hint in &result.hints {
140 let pad = max_w.saturating_sub(hint.len());
141 println!(
142 " {} \u{2502} {}{} \u{2502}",
143 "\u{2502}".dimmed(),
144 hint,
145 " ".repeat(pad)
146 );
147 }
148 println!(
149 " {} \u{2570}{}\u{256F}",
150 "\u{2502}".dimmed(),
151 dashes.dimmed()
152 );
153 }
154}
155
156pub fn print_phase_skipped(name: &str, reason: &str) {
162 let name_padded = format!("{:<16}", name);
163 println!(
164 " {} {} {}",
165 icons::skipped(),
166 name_padded,
167 format!("skipped ({})", reason).dimmed()
168 );
169}
170
171pub fn print_mcp_validation(results: &[McpCheckResult]) {
186 let w = term_width();
187
188 println!();
189 let label = "\u{2500}\u{2500} MCP Validation ";
190 let fill = "\u{2500}".repeat(w.saturating_sub(label.len() + 2));
191 println!(" {}{}", label.dimmed(), fill.dimmed());
192 println!();
193
194 for result in results {
195 println!(" {} {}", icons::mcp(), result.server_name.green().bold());
196
197 let conn_info = format!("connected \u{00B7} {} tools available", result.tool_count);
199 let dur_str = format!("{}ms", result.connect_ms);
200 let conn_pad = w.saturating_sub(
201 4 + conn_info.len() + dur_str.len() + 2,
203 );
204 println!(
205 " {} {} \u{00B7} {} tools available{}{}",
206 "\u{2502}".dimmed(),
207 "connected".green(),
208 result.tool_count,
209 " ".repeat(conn_pad),
210 dur_str.dimmed()
211 );
212 println!(" {}", "\u{2502}".dimmed());
213
214 let mut valid_count = 0u32;
215 let total = result.validations.len() as u32;
216
217 for v in &result.validations {
218 if v.valid {
219 valid_count += 1;
220 println!(
221 " {} {} {:<14}\u{2192} {:<24} {}",
222 "\u{2502}".dimmed(),
223 icons::success(),
224 v.task_id,
225 v.tool_name,
226 "params valid".dimmed()
227 );
228 } else {
229 println!(
230 " {} {} {:<14}\u{2192} {:<24} {}",
231 "\u{2502}".dimmed(),
232 icons::failed(),
233 v.task_id.red(),
234 v.tool_name,
235 format!("{} errors", v.errors.len()).red()
236 );
237 for err in &v.errors {
238 println!(
239 " {} {} {} {}",
240 "\u{2502}".dimmed(),
241 "\u{2502}".dimmed(),
242 format!("[{}]", err.path).yellow(),
243 err.message.dimmed()
244 );
245 }
246 }
247 }
248
249 println!(" {}", "\u{2502}".dimmed());
250 let summary = format!("{}/{} calls valid", valid_count, total);
251 let summary_colored = if valid_count == total {
252 summary.green()
253 } else {
254 summary.yellow()
255 };
256 println!(" {} {}", "\u{2502}".dimmed(), summary_colored);
257 println!();
258 }
259}
260
261#[allow(clippy::too_many_arguments)]
273pub fn print_check_summary(
274 valid: bool,
275 total_ms: u64,
276 task_count: usize,
277 edge_count: usize,
278 layer_count: usize,
279 schema_count: u32,
280 strict_info: Option<(u32, u32, u32)>, error_codes: &[(&str, &str)], ) {
283 let w = term_width();
284 let inner = w - 2;
285 let border = "\u{2500}".repeat(inner);
286
287 println!("\u{256D}{}\u{256E}", border.dimmed());
288 println!("\u{2502}{}\u{2502}", " ".repeat(inner));
289
290 let (icon, label) = if valid {
292 (icons::success(), "V A L I D".green().bold())
293 } else {
294 (icons::failed(), "I N V A L I D".red().bold())
295 };
296 let dur = format!("{}ms", total_ms);
297 let status_line = format!(" {} {}", icon, label);
298 let pad = inner.saturating_sub(stripped_len(&status_line) + dur.len() + 2);
299 println!(
300 "\u{2502}{}{}{} \u{2502}",
301 status_line,
302 " ".repeat(pad),
303 dur.dimmed()
304 );
305 println!("\u{2502}{}\u{2502}", " ".repeat(inner));
306
307 let mut stats_parts = vec![
309 format!(
310 "{} {}",
311 task_count,
312 if task_count == 1 { "task" } else { "tasks" }
313 ),
314 format!(
315 "{} {}",
316 edge_count,
317 if edge_count == 1 { "edge" } else { "edges" }
318 ),
319 format!(
320 "{} {}",
321 layer_count,
322 if layer_count == 1 { "layer" } else { "layers" }
323 ),
324 ];
325 if schema_count > 0 {
326 stats_parts.push(format!("{} schemas", schema_count));
327 }
328 let stats = stats_parts.join(" \u{00B7} ");
329 let stats_pad = inner.saturating_sub(stats.len() + 2);
330 println!("\u{2502} {}{}\u{2502}", stats, " ".repeat(stats_pad));
331
332 if let Some((valid_calls, total_calls, param_errors)) = strict_info {
334 let strict_line = format!(
335 "strict: {}/{} MCP calls valid \u{00B7} {} param errors",
336 valid_calls, total_calls, param_errors
337 );
338 let strict_pad = inner.saturating_sub(strict_line.len() + 2);
339 println!(
340 "\u{2502} {}{}\u{2502}",
341 strict_line,
342 " ".repeat(strict_pad)
343 );
344 }
345
346 for (code, msg) in error_codes {
348 let err_line = format!("{}: {}", code, msg);
349 let err_pad = inner.saturating_sub(err_line.len() + 2);
350 println!(
351 "\u{2502} {}{}\u{2502}",
352 err_line.red(),
353 " ".repeat(err_pad)
354 );
355 }
356
357 println!("\u{2502}{}\u{2502}", " ".repeat(inner));
358 println!("\u{2570}{}\u{256F}", border.dimmed());
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364
365 #[test]
366 fn test_phase_result_pass() {
367 let result = PhaseResult {
368 name: "schema",
369 passed: true,
370 detail: "YAML valid against @0.12".to_string(),
371 duration_ms: 1,
372 errors: vec![],
373 hints: vec![],
374 };
375 print_phase(&result);
377 }
378
379 #[test]
380 fn test_phase_result_fail_with_hints() {
381 let result = PhaseResult {
382 name: "dag",
383 passed: false,
384 detail: "CYCLE DETECTED".to_string(),
385 duration_ms: 0,
386 errors: vec!["step_a \u{2192} step_b \u{2192} step_c \u{2192} step_a".to_string()],
387 hints: vec![
388 "Remove one dependency to break the cycle.".to_string(),
389 "Common fix: use with: binding instead of depends_on.".to_string(),
390 ],
391 };
392 print_phase(&result);
393 }
394
395 #[test]
396 fn test_stripped_len_plain() {
397 assert_eq!(stripped_len("hello"), 5);
399 assert_eq!(stripped_len(""), 0);
400 assert_eq!(stripped_len("V A L I D"), 9);
401 }
402
403 #[test]
404 fn test_stripped_len_colored_crate() {
405 use colored::Colorize;
407 let green = "hello".green().to_string();
408 assert_eq!(stripped_len(&green), 5);
409 let bold_green = "\u{2713}".green().bold().to_string();
410 assert_eq!(stripped_len(&bold_green), 1);
411 }
412}