Files
rtk-ai__rtk/src/tree.rs
T
patrick szymkowiak 8fae5b067d Release v0.31.0 — develop → master (#666)
* fix: P1 exit codes, grep regex perf, SQLite WAL (#631)

* fix: P1 exit codes, grep regex perf, SQLite concurrency

Exit code propagation (same pattern as existing modules):
- wget_cmd: run() and run_stdout() now exit on failure
- container: docker_logs, kubectl_pods/services/logs now check
  status before parsing JSON (was showing "No pods found" on error)
- pnpm_cmd: replace bail!() with eprint + process::exit in
  run_list and run_install

Performance:
- grep_cmd: compile context regex once before loop instead of
  per-line in clean_line() (was N compilations per grep call)

Data integrity:
- tracking: add PRAGMA journal_mode=WAL and busy_timeout=5000
  to prevent SQLite corruption with concurrent Claude Code instances

Signed-off-by: Patrick <patrick@rtk.ai>
Signed-off-by: Patrick szymkowiak <patrick.szymkowiak@innovtech.eu>

* fix: address review findings on P1 fixes

- tracking: WAL pragma non-fatal (NFS/read-only compat)
- wget: forward raw stderr on failure, track raw==raw (no fake savings)
- container: remove stderr shadow in docker_logs, add empty-stderr
  guard on all 4 new exit code paths for consistency with prisma pattern

Signed-off-by: Patrick <patrick@rtk.ai>
Signed-off-by: Patrick szymkowiak <patrick.szymkowiak@innovtech.eu>

---------

Signed-off-by: Patrick <patrick@rtk.ai>
Signed-off-by: Patrick szymkowiak <patrick.szymkowiak@innovtech.eu>

* fix: raise output caps for P0 bugs (#617, #618, #620) (#630)

* fix: raise output caps for grep, git status, and parser fallback (#617, #618, #620)

- grep: per-file match cap 10 → 25, global max 50 → 200
- git status: file list caps 5/5/3 → 15/15/10
- parser fallback: truncate 500 → 2000 chars across all modules

These P0 bugs caused LLM retry loops when RTK returned less signal
than the raw command, making RTK worse than not using it.

Fixes #617, #618, #620

Signed-off-by: Patrick <patrick@rtk.ai>
Signed-off-by: Patrick szymkowiak <patrick.szymkowiak@innovtech.eu>

* fix: update README example and add truncation tests for modified/untracked

- parser/README.md: update example from 500 → 2000 to match code
- git.rs: add test_format_status_modified_truncation (cap 15)
- git.rs: add test_format_status_untracked_truncation (cap 10)

Signed-off-by: Patrick <patrick@rtk.ai>
Signed-off-by: Patrick szymkowiak <patrick.szymkowiak@innovtech.eu>

* refactor: extract output caps into [limits] config section

Move hardcoded caps into config.toml so users can tune them:

  [limits]
  grep_max_results = 200      # global grep match limit
  grep_max_per_file = 25      # per-file match limit
  status_max_files = 15       # staged/modified file list cap
  status_max_untracked = 10   # untracked file list cap
  passthrough_max_chars = 2000 # parser fallback truncation

All 8 modules now read from config::limits() instead of hardcoded
values. Defaults unchanged from previous commit.

Signed-off-by: Patrick <patrick@rtk.ai>
Signed-off-by: Patrick szymkowiak <patrick.szymkowiak@innovtech.eu>

---------

Signed-off-by: Patrick <patrick@rtk.ai>
Signed-off-by: Patrick szymkowiak <patrick.szymkowiak@innovtech.eu>

* feat(.claude): add /rtk-triage skill — cross-analysis of PRs and issues (#662)

* feat(.claude): add /rtk-triage skill — orchestrated PR+issue cross-analysis

New skill that runs issue-triage + pr-triage in parallel then produces
a cross-analysis layer that neither skill can do individually:

- Double coverage detection: identifies when 2+ PRs target the same issue
  (via body scan + file overlap), recommends which to keep/close
- Security gap detection: for security review issues, maps each finding
  to a PR (or flags it as uncovered)
- P0/P1 bugs without PR: groups by pattern to suggest sprint batching
- Our dirty PRs: identifies probable cause (conflict with sibling PR,
  needs rebase, missing linked issue)

Output is saved automatically to claudedocs/RTK-YYYY-MM-DD.md.

Usage: /rtk-triage           (French, auto-save)
       /rtk-triage en        (English output)

Signed-off-by: Florian Bruniaux <florian@bel-etage.com>
Signed-off-by: Florian BRUNIAUX <florian@bruniaux.com>

* docs(architecture): update module count to 66

Sync ARCHITECTURE.md with current main.rs state.
Previous count (60) was stale since several modules were added
(dotnet_cmd, dotnet_format_report, dotnet_trx, npm_cmd, gt_cmd, etc.).

Signed-off-by: Florian Bruniaux <florian@bel-etage.com>
Signed-off-by: Florian BRUNIAUX <florian@bruniaux.com>

---------

Signed-off-by: Florian Bruniaux <florian@bel-etage.com>
Signed-off-by: Florian BRUNIAUX <florian@bruniaux.com>

* fix: subcommand routing drops unrecognized subcommands (#600) (#601)

- git stash: pass unknown subcommands (save, branch, clear) through
  instead of silently falling back to git stash push
- git branch: add --show-current, --set-upstream-to, --format, --sort
  to flag detection so they don't get overridden by -a injection
- pip: replace bail!() with passthrough for unknown subcommands
  (freeze, download, wheel, etc.)

Fixes #600

Signed-off-by: Patrick szymkowiak <patrick.szymkowiak@innovtech.eu>

* fix: resolve cargo fmt + 54 clippy warnings blocking CI (#663)

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>

* fix: discover absolute paths + git global options (#485, #163) (#518)

* fix: discover classifies absolute paths like /usr/bin/grep (#485)

Normalize absolute binary paths before classification:
/usr/bin/grep → grep, /bin/ls → ls, /usr/local/bin/git → git

Adds strip_absolute_path() helper + 5 tests.

Signed-off-by: Patrick szymkowiak <patrick.szymkowiak@innovtech.eu>

* fix: discover and rewrite support git global options -C, --no-pager, etc. (#163)

Strip git global options (-C <path>, -c <key=val>, --git-dir, --work-tree,
--no-pager, --no-optional-locks, --bare, --literal-pathspecs) before
classification so git -C /tmp status is recognized as rtk git.

Rewrite preserves global options: git -C /tmp status → rtk git -C /tmp status

Adds GIT_GLOBAL_OPT lazy_static regex + strip_git_global_opts() helper + 6 tests.

Signed-off-by: Patrick szymkowiak <patrick.szymkowiak@innovtech.eu>

---------

Signed-off-by: Patrick szymkowiak <patrick.szymkowiak@innovtech.eu>

* fix: prevent double `--` separator in cargo clippy with -p flags (#519)

When running `rtk cargo clippy -p my-crate -- -D warnings`, Clap with
`trailing_var_arg = true` preserves the `--` in parsed args when flags
precede it. `restore_double_dash()` then added a second `--`, producing
`cargo clippy -p my-crate -- -- -D warnings`. This caused rustc to
interpret `-D` as a filename instead of a lint flag.

Fix: skip restoration when args already contain `--` (Clap preserved it).

Fixes #496

Signed-off-by: Ousama Ben Younes <benyounes.ousama@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* ci: add PR template + target branch check (#521)

- PR template reminds contributors to target develop
- CI workflow labels PRs targeting master with 'wrong-base' and posts a comment
- Excludes develop→master PRs (maintainer releases)

Signed-off-by: Patrick <patrick@rtk-ai.com>
Signed-off-by: Patrick szymkowiak <patrick.szymkowiak@innovtech.eu>

* fix: prevent rtk read from corrupting JSON/YAML/data files (#522)

Add Language::Data variant for data formats (JSON, YAML, TOML, XML, CSV, etc.)
with empty comment patterns to prevent comment stripping. AggressiveFilter
falls back to MinimalFilter for data files.

Fixes #464

Signed-off-by: Ousama Ben Younes <benyounes.ousama@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix: skip rewriting find/fd in pipes to preserve xargs compatibility (#439) (#563)

rtk find outputs a grouped format incompatible with pipe consumers
like xargs, grep, wc, sort. Skip rewrite when find/fd is followed
by a pipe, preserving native one-per-line output.

Signed-off-by: Patrick szymkowiak <patrick.szymkowiak@innovtech.eu>

* fix: add hint when git diff is truncated + fix --no-compact passthrough (#427) (#564)

When compact_diff truncates output, append a hint line so Claude knows
how to get the full diff: [full diff: rtk git diff --no-compact]

Also fix --no-compact flag being passed to git (causing usage error)
and remove decorative emoji from compact_diff output.

Signed-off-by: Patrick szymkowiak <patrick.szymkowiak@innovtech.eu>

* fix: propagate exit codes in git diff, status+args, commit, and branch (#632)

4 P1 bugs where git exit codes were swallowed:
- git diff: failure silently printed empty stat output
- git status (with args): failure was filtered instead of propagated
- git commit: failure printed "FAILED" but returned Ok(()) breaking pre-commit hooks
- git branch (list mode): failure was silently ignored

All now follow the established pattern: eprint stderr, track raw==raw, process::exit(code).

Signed-off-by: Patrick szymkowiak <patrick.szymkowiak@innovtech.eu>

* feat: add 5 new TOML filters (ollama, nx, gradle, spring-boot, jira) (#635)

* feat: add 5 new TOML built-in filters (ollama, nx, gradle, spring-boot, jira)

New filters for commands not covered by Rust modules:
- ollama: strip ANSI spinners, keep final text response (#624)
- nx: strip Nx monorepo noise, keep build results (#444)
- gradle/gradlew: strip UP-TO-DATE tasks, keep build summary (#147)
- spring-boot: strip banner and verbose logs, keep startup/errors (#147)
- jira: strip blanks, truncate wide columns (#524)

All 5 filters pass inline tests via rtk verify (123/123).
Updated builtin filter count: 47 -> 52.

Signed-off-by: Patrick szymkowiak <patrick.szymkowiak@innovtech.eu>

* feat: add 5 more TOML filters (turbo, mise, just, task, yadm)

New filters for task runners and git wrapper:
- turbo: strip cache/Tasks/Duration noise, keep task output (#531)
- mise: strip install/download progress, keep task results (#607)
- just: strip blanks and recipe headers, keep output (#607)
- task: strip task headers and up-to-date lines, keep results (#607)
- yadm: strip hint lines, compact git-like output (#567)

All verified with fake binaries through catch-all TOML engine.
137/137 TOML tests pass, 934 Rust tests pass.
Updated builtin filter count: 52 -> 57.

Signed-off-by: Patrick szymkowiak <patrick.szymkowiak@innovtech.eu>

---------

Signed-off-by: Patrick szymkowiak <patrick.szymkowiak@innovtech.eu>

* fix: replace emojis with plain text in git status output (#603) (#638)

Git status output used emojis (📌, 📝, ❓, ✅, ⚠️) that confuse
non-Claude LLMs (GPT, etc.) causing retry loops. Replace with plain
text labels (branch:, modified:, staged:, untracked:, conflicts:).

Also add "clean — nothing to commit" when working tree is clean,
so LLMs understand the repo state without ambiguity.

Before: 📌 master
After:  branch: master
        clean — nothing to commit

Fixes #603

Signed-off-by: Patrick szymkowiak <patrick.szymkowiak@innovtech.eu>

---------

Signed-off-by: Patrick <patrick@rtk.ai>
Signed-off-by: Patrick szymkowiak <patrick.szymkowiak@innovtech.eu>
Signed-off-by: Florian Bruniaux <florian@bel-etage.com>
Signed-off-by: Florian BRUNIAUX <florian@bruniaux.com>
Signed-off-by: Patrick Szymkowiak <patrick@rtk-ai.app>
Signed-off-by: Ousama Ben Younes <benyounes.ousama@gmail.com>
Signed-off-by: Patrick <patrick@rtk-ai.com>
Co-authored-by: Florian BRUNIAUX <florian@bruniaux.com>
Co-authored-by: Ben Younes <benyounes.ousama@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 09:19:11 +01:00

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"));
}
}