Files
rtk-ai__rtk/src/summary.rs
Florian BRUNIAUX 60d2d252ef fix: P0 crashes + cargo check + dedup utilities + discover status
Phase 1: Fix P0 crashes (3 bugs)
- Add external_subcommand fallback for cargo/docker/kubectl
- Fixes crashes on unsupported subcommands (e.g., cargo fmt, docker build)
- All passthroughs now route gracefully instead of Clap parse errors

Phase 2: cargo check handler + refactor (-40 lines)
- Add dedicated cargo check handler (most frequently used missing command)
- Extract run_cargo_filtered() to eliminate 90 lines of duplication
- Fix filter_cargo_build() to handle "Checking" prefix (not just "Compiling")
- Normalize exit code propagation in run_test()

Phase 3: Utility deduplication (-60 lines)
- Fix utils::truncate() Unicode handling (char-aware, not byte-based)
- Remove 5 duplicated utility functions across modules:
  - 3x truncate() → use crate::utils::truncate
  - 1x strip_ansi() → use crate::utils::strip_ansi
- Replace ~60 lines of inline package manager detection with utils::package_manager_exec()
- Clean: lint_cmd, prettier_cmd, playwright_cmd now use shared utils

Phase 4: Discover reporting improvements
- Add RtkStatus enum (Existing/Passthrough/NotSupported)
- Show status column in discover report to distinguish handler types
- Prevents future misleading "build 9 git subcommands" false analyses

Cleanup:
- Remove 2 unused imports (parser::ParseError, std::collections::HashMap)
- Reduce clippy warnings from 23 to 19

Testing:
- 220 tests pass
- 88 smoke tests pass
- All manual verification complete

Net: +10 lines, -3 P0 bugs, -5 duplications, +1 feature, +1 enum

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-06 10:37:50 +01:00

298 lines
9.0 KiB
Rust
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use crate::tracking;
use crate::utils::truncate;
use anyhow::{Context, Result};
use regex::Regex;
use std::process::{Command, Stdio};
/// Run a command and provide a heuristic summary
pub fn run(command: &str, verbose: u8) -> Result<()> {
let timer = tracking::TimedExecution::start();
if verbose > 0 {
eprintln!("Running and summarizing: {}", command);
}
let output = if cfg!(target_os = "windows") {
Command::new("cmd")
.args(["/C", command])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
} else {
Command::new("sh")
.args(["-c", command])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
}
.context("Failed to execute command")?;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let raw = format!("{}\n{}", stdout, stderr);
let summary = summarize_output(&raw, command, output.status.success());
println!("{}", summary);
timer.track(command, "rtk summary", &raw, &summary);
Ok(())
}
fn summarize_output(output: &str, command: &str, success: bool) -> String {
let lines: Vec<&str> = output.lines().collect();
let mut result = Vec::new();
// Status
let status_icon = if success { "✅" } else { "❌" };
result.push(format!(
"{} Command: {}",
status_icon,
truncate(command, 60)
));
result.push(format!(" {} lines of output", lines.len()));
result.push(String::new());
// Detect type of output and summarize accordingly
let output_type = detect_output_type(output, command);
match output_type {
OutputType::TestResults => summarize_tests(output, &mut result),
OutputType::BuildOutput => summarize_build(output, &mut result),
OutputType::LogOutput => summarize_logs_quick(output, &mut result),
OutputType::ListOutput => summarize_list(output, &mut result),
OutputType::JsonOutput => summarize_json(output, &mut result),
OutputType::Generic => summarize_generic(output, &mut result),
}
result.join("\n")
}
#[derive(Debug)]
enum OutputType {
TestResults,
BuildOutput,
LogOutput,
ListOutput,
JsonOutput,
Generic,
}
fn detect_output_type(output: &str, command: &str) -> OutputType {
let cmd_lower = command.to_lowercase();
let out_lower = output.to_lowercase();
if cmd_lower.contains("test") || out_lower.contains("passed") && out_lower.contains("failed") {
OutputType::TestResults
} else if cmd_lower.contains("build")
|| cmd_lower.contains("compile")
|| out_lower.contains("compiling")
{
OutputType::BuildOutput
} else if out_lower.contains("error:")
|| out_lower.contains("warn:")
|| out_lower.contains("[info]")
{
OutputType::LogOutput
} else if output.trim_start().starts_with('{') || output.trim_start().starts_with('[') {
OutputType::JsonOutput
} else if output.lines().all(|l| {
l.len() < 200
&& !l
.contains('\t')
.then_some(true)
.unwrap_or(l.split_whitespace().count() < 10)
}) {
OutputType::ListOutput
} else {
OutputType::Generic
}
}
fn summarize_tests(output: &str, result: &mut Vec<String>) {
result.push("📋 Test Results:".to_string());
let mut passed = 0;
let mut failed = 0;
let mut skipped = 0;
let mut failures = Vec::new();
for line in output.lines() {
let lower = line.to_lowercase();
if lower.contains("passed") || lower.contains("✓") || lower.contains("ok") {
// Try to extract number
if let Some(n) = extract_number(&lower, "passed") {
passed = n;
} else {
passed += 1;
}
}
if lower.contains("failed") || lower.contains("✗") || lower.contains("fail") {
if let Some(n) = extract_number(&lower, "failed") {
failed = n;
}
if !line.contains("0 failed") {
failures.push(line.to_string());
}
}
if lower.contains("skipped") || lower.contains("ignored") {
if let Some(n) = extract_number(&lower, "skipped").or(extract_number(&lower, "ignored"))
{
skipped = n;
}
}
}
result.push(format!(" ✅ {} passed", passed));
if failed > 0 {
result.push(format!(" ❌ {} failed", failed));
}
if skipped > 0 {
result.push(format!(" ⏭️ {} skipped", skipped));
}
if !failures.is_empty() {
result.push(String::new());
result.push(" Failures:".to_string());
for f in failures.iter().take(5) {
result.push(format!(" • {}", truncate(f, 70)));
}
}
}
fn summarize_build(output: &str, result: &mut Vec<String>) {
result.push("🔨 Build Summary:".to_string());
let mut errors = 0;
let mut warnings = 0;
let mut compiled = 0;
let mut error_msgs = Vec::new();
for line in output.lines() {
let lower = line.to_lowercase();
if lower.contains("error") && !lower.contains("0 error") {
errors += 1;
if error_msgs.len() < 5 {
error_msgs.push(line.to_string());
}
}
if lower.contains("warning") && !lower.contains("0 warning") {
warnings += 1;
}
if lower.contains("compiling") || lower.contains("compiled") {
compiled += 1;
}
}
if compiled > 0 {
result.push(format!(" 📦 {} crates/files compiled", compiled));
}
if errors > 0 {
result.push(format!(" ❌ {} errors", errors));
}
if warnings > 0 {
result.push(format!(" ⚠️ {} warnings", warnings));
}
if errors == 0 && warnings == 0 {
result.push(" ✅ Build successful".to_string());
}
if !error_msgs.is_empty() {
result.push(String::new());
result.push(" Errors:".to_string());
for e in &error_msgs {
result.push(format!(" • {}", truncate(e, 70)));
}
}
}
fn summarize_logs_quick(output: &str, result: &mut Vec<String>) {
result.push("📝 Log Summary:".to_string());
let mut errors = 0;
let mut warnings = 0;
let mut info = 0;
for line in output.lines() {
let lower = line.to_lowercase();
if lower.contains("error") || lower.contains("fatal") {
errors += 1;
} else if lower.contains("warn") {
warnings += 1;
} else if lower.contains("info") {
info += 1;
}
}
result.push(format!(" ❌ {} errors", errors));
result.push(format!(" ⚠️ {} warnings", warnings));
result.push(format!(" ℹ️ {} info", info));
}
fn summarize_list(output: &str, result: &mut Vec<String>) {
let lines: Vec<&str> = output.lines().filter(|l| !l.trim().is_empty()).collect();
result.push(format!("📋 List ({} items):", lines.len()));
for line in lines.iter().take(10) {
result.push(format!(" • {}", truncate(line, 70)));
}
if lines.len() > 10 {
result.push(format!(" ... +{} more", lines.len() - 10));
}
}
fn summarize_json(output: &str, result: &mut Vec<String>) {
result.push("📋 JSON Output:".to_string());
// Try to parse and show structure
if let Ok(value) = serde_json::from_str::<serde_json::Value>(output) {
match &value {
serde_json::Value::Array(arr) => {
result.push(format!(" Array with {} items", arr.len()));
}
serde_json::Value::Object(obj) => {
result.push(format!(" Object with {} keys:", obj.len()));
for key in obj.keys().take(10) {
result.push(format!(" • {}", key));
}
if obj.len() > 10 {
result.push(format!(" ... +{} more keys", obj.len() - 10));
}
}
_ => {
result.push(format!(" {}", truncate(&value.to_string(), 100)));
}
}
} else {
result.push(" (Invalid JSON)".to_string());
}
}
fn summarize_generic(output: &str, result: &mut Vec<String>) {
let lines: Vec<&str> = output.lines().collect();
result.push("📋 Output:".to_string());
// First few lines
for line in lines.iter().take(5) {
if !line.trim().is_empty() {
result.push(format!(" {}", truncate(line, 75)));
}
}
if lines.len() > 10 {
result.push(" ...".to_string());
// Last few lines
for line in lines.iter().skip(lines.len() - 3) {
if !line.trim().is_empty() {
result.push(format!(" {}", truncate(line, 75)));
}
}
}
}
fn extract_number(text: &str, after: &str) -> Option<usize> {
let re = Regex::new(&format!(r"(\d+)\s*{}", after)).ok()?;
re.captures(text)
.and_then(|c| c.get(1))
.and_then(|m| m.as_str().parse().ok())
}