mirror of
https://github.com/rtk-ai/rtk.git
synced 2026-09-19 07:33:17 +08:00
810d7ffb63
cargo fmt diffs in config.rs, git.rs, playwright_cmd.rs were failing the fmt CI check, which cascaded to block clippy/test/security on PRs #632, #635, #638. Also fixes all clippy warnings: dead code annotations, iterator simplifications, assert patterns, and unnecessary allocations. Signed-off-by: Patrick Szymkowiak <patrick@rtk-ai.app> Signed-off-by: Patrick szymkowiak <patrick.szymkowiak@innovtech.eu>
209 lines
6.0 KiB
Rust
209 lines
6.0 KiB
Rust
//! tree command - proxy to native tree with token-optimized output
|
|
//!
|
|
//! This module proxies to the native `tree` command and filters the output
|
|
//! to reduce token usage while preserving structure visibility.
|
|
//!
|
|
//! Token optimization: automatically excludes noise directories via -I pattern
|
|
//! unless -a flag is present (respecting user intent).
|
|
|
|
use crate::tracking;
|
|
use crate::utils::{resolved_command, tool_exists};
|
|
use anyhow::{Context, Result};
|
|
|
|
/// Noise directories commonly excluded from LLM context
|
|
const NOISE_DIRS: &[&str] = &[
|
|
"node_modules",
|
|
".git",
|
|
"target",
|
|
"__pycache__",
|
|
".next",
|
|
"dist",
|
|
"build",
|
|
".cache",
|
|
".turbo",
|
|
".vercel",
|
|
".pytest_cache",
|
|
".mypy_cache",
|
|
".tox",
|
|
".venv",
|
|
"venv",
|
|
"env",
|
|
".env",
|
|
"coverage",
|
|
".nyc_output",
|
|
".DS_Store",
|
|
"Thumbs.db",
|
|
".idea",
|
|
".vscode",
|
|
".vs",
|
|
"*.egg-info",
|
|
".eggs",
|
|
];
|
|
|
|
pub fn run(args: &[String], verbose: u8) -> Result<()> {
|
|
let timer = tracking::TimedExecution::start();
|
|
|
|
// Check if tree is installed
|
|
if !tool_exists("tree") {
|
|
anyhow::bail!(
|
|
"tree command not found. Install it first:\n\
|
|
- macOS: brew install tree\n\
|
|
- Ubuntu/Debian: sudo apt install tree\n\
|
|
- Fedora/RHEL: sudo dnf install tree\n\
|
|
- Arch: sudo pacman -S tree"
|
|
);
|
|
}
|
|
|
|
let mut cmd = resolved_command("tree");
|
|
|
|
// Determine if user wants all files or default behavior
|
|
let show_all = args.iter().any(|a| a == "-a" || a == "--all");
|
|
let has_ignore = args.iter().any(|a| a == "-I" || a.starts_with("--ignore="));
|
|
|
|
// Auto-inject -I pattern unless user wants all or already specified -I
|
|
if !show_all && !has_ignore {
|
|
let ignore_pattern = NOISE_DIRS.join("|");
|
|
cmd.arg("-I").arg(&ignore_pattern);
|
|
}
|
|
|
|
// Pass all user args
|
|
for arg in args {
|
|
cmd.arg(arg);
|
|
}
|
|
|
|
let output = cmd.output().context("Failed to run tree")?;
|
|
|
|
if !output.status.success() {
|
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
|
eprint!("{}", stderr);
|
|
std::process::exit(output.status.code().unwrap_or(1));
|
|
}
|
|
|
|
let raw = String::from_utf8_lossy(&output.stdout).to_string();
|
|
let filtered = filter_tree_output(&raw);
|
|
|
|
if verbose > 0 {
|
|
eprintln!(
|
|
"Lines: {} → {} ({}% reduction)",
|
|
raw.lines().count(),
|
|
filtered.lines().count(),
|
|
if raw.lines().count() > 0 {
|
|
100 - (filtered.lines().count() * 100 / raw.lines().count())
|
|
} else {
|
|
0
|
|
}
|
|
);
|
|
}
|
|
|
|
print!("{}", filtered);
|
|
timer.track("tree", "rtk tree", &raw, &filtered);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn filter_tree_output(raw: &str) -> String {
|
|
let lines: Vec<&str> = raw.lines().collect();
|
|
|
|
if lines.is_empty() {
|
|
return "\n".to_string();
|
|
}
|
|
|
|
let mut filtered_lines = Vec::new();
|
|
|
|
for line in lines {
|
|
// Skip the final summary line (e.g., "5 directories, 23 files")
|
|
if line.contains("director") && line.contains("file") {
|
|
continue;
|
|
}
|
|
|
|
// Skip empty lines at the end
|
|
if line.trim().is_empty() && filtered_lines.is_empty() {
|
|
continue;
|
|
}
|
|
|
|
filtered_lines.push(line);
|
|
}
|
|
|
|
// Remove trailing empty lines
|
|
while filtered_lines.last().is_some_and(|l| l.trim().is_empty()) {
|
|
filtered_lines.pop();
|
|
}
|
|
|
|
filtered_lines.join("\n") + "\n"
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_filter_removes_summary() {
|
|
let input = ".\n├── src\n│ └── main.rs\n└── Cargo.toml\n\n2 directories, 3 files\n";
|
|
let output = filter_tree_output(input);
|
|
assert!(!output.contains("directories"));
|
|
assert!(!output.contains("files"));
|
|
assert!(output.contains("main.rs"));
|
|
assert!(output.contains("Cargo.toml"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_filter_preserves_structure() {
|
|
let input = ".\n├── src\n│ ├── main.rs\n│ └── lib.rs\n└── tests\n └── test.rs\n";
|
|
let output = filter_tree_output(input);
|
|
assert!(output.contains("├──"));
|
|
assert!(output.contains("│"));
|
|
assert!(output.contains("└──"));
|
|
assert!(output.contains("main.rs"));
|
|
assert!(output.contains("test.rs"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_filter_handles_empty() {
|
|
let input = "";
|
|
let output = filter_tree_output(input);
|
|
assert_eq!(output, "\n");
|
|
}
|
|
|
|
#[test]
|
|
fn test_filter_removes_trailing_empty_lines() {
|
|
let input = ".\n├── file.txt\n\n\n";
|
|
let output = filter_tree_output(input);
|
|
assert_eq!(output.matches('\n').count(), 2); // Root + file.txt + final newline
|
|
}
|
|
|
|
#[test]
|
|
fn test_filter_summary_variations() {
|
|
// Test different summary formats
|
|
let inputs = vec![
|
|
(".\n└── file.txt\n\n0 directories, 1 file\n", "1 file"),
|
|
(".\n└── file.txt\n\n1 directory, 0 files\n", "1 directory"),
|
|
(".\n└── file.txt\n\n10 directories, 25 files\n", "25 files"),
|
|
];
|
|
|
|
for (input, summary_fragment) in inputs {
|
|
let output = filter_tree_output(input);
|
|
assert!(
|
|
!output.contains(summary_fragment),
|
|
"Should remove summary '{}' from output",
|
|
summary_fragment
|
|
);
|
|
assert!(
|
|
output.contains("file.txt"),
|
|
"Should preserve file.txt in output"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_noise_dirs_constant() {
|
|
// Verify NOISE_DIRS contains expected patterns
|
|
assert!(NOISE_DIRS.contains(&"node_modules"));
|
|
assert!(NOISE_DIRS.contains(&".git"));
|
|
assert!(NOISE_DIRS.contains(&"target"));
|
|
assert!(NOISE_DIRS.contains(&"__pycache__"));
|
|
assert!(NOISE_DIRS.contains(&".next"));
|
|
assert!(NOISE_DIRS.contains(&"dist"));
|
|
assert!(NOISE_DIRS.contains(&"build"));
|
|
}
|
|
}
|