Skip to main content

st/
st_unified.rs

1// ST Unified Tool System - Replace all traditional tools with Smart Tree!
2// "Why use 20 tools when ST can do it all?" - The Cheet 🎸
3
4use anyhow::{Context, Result};
5use std::path::{Path, PathBuf};
6use std::process::Command;
7
8/// Unified ST interface for all file operations
9pub struct StUnified {
10    st_binary: PathBuf,
11}
12
13impl StUnified {
14    pub fn new() -> Result<Self> {
15        // Find st binary
16        let st_binary = std::env::current_exe()
17            .ok()
18            .and_then(|p| {
19                let dir = p.parent()?;
20                let st = dir.join("st");
21                if st.exists() {
22                    Some(st)
23                } else {
24                    None
25                }
26            })
27            .unwrap_or_else(|| PathBuf::from("./target/release/st"));
28
29        Ok(Self { st_binary })
30    }
31
32    /// List files (replaces LS tool)
33    pub fn ls(&self, path: &Path, pattern: Option<&str>) -> Result<String> {
34        let mut cmd = Command::new(&self.st_binary);
35        cmd.arg("--mode")
36            .arg("ls")
37            .arg("--depth")
38            .arg("1")
39            .arg("--no-emoji")
40            .arg(path);
41
42        if let Some(pat) = pattern {
43            cmd.arg("--find").arg(pat);
44        }
45
46        let output = cmd.output().context("Failed to run st for ls")?;
47
48        Ok(String::from_utf8_lossy(&output.stdout).to_string())
49    }
50
51    /// Read file (replaces Read tool)
52    pub fn read(&self, path: &Path, offset: Option<usize>, limit: Option<usize>) -> Result<String> {
53        // ST doesn't read file contents, so use standard fs
54        let content = std::fs::read_to_string(path).context("Failed to read file")?;
55
56        let lines: Vec<&str> = content.lines().collect();
57        let start = offset.unwrap_or(0);
58        let end = start + limit.unwrap_or(lines.len());
59
60        Ok(lines[start.min(lines.len())..end.min(lines.len())]
61            .iter()
62            .enumerate()
63            .map(|(i, line)| format!("{:6}→{}", start + i + 1, line))
64            .collect::<Vec<_>>()
65            .join("\n"))
66    }
67
68    /// Search in files (replaces Grep tool)
69    pub fn grep(&self, pattern: &str, path: &Path, file_type: Option<&str>) -> Result<String> {
70        let mut cmd = Command::new(&self.st_binary);
71        cmd.arg("--search")
72            .arg(pattern)
73            .arg("--mode")
74            .arg("ai")
75            .arg("--depth")
76            .arg("0")
77            .arg(path);
78
79        if let Some(ft) = file_type {
80            cmd.arg("--type").arg(ft);
81        }
82
83        let output = cmd.output().context("Failed to run st for search")?;
84
85        Ok(String::from_utf8_lossy(&output.stdout).to_string())
86    }
87
88    /// Find files by pattern (replaces Glob tool)
89    pub fn glob(&self, pattern: &str, path: &Path) -> Result<String> {
90        let output = Command::new(&self.st_binary)
91            .arg("--find")
92            .arg(pattern)
93            .arg("--mode")
94            .arg("json")
95            .arg("--depth")
96            .arg("0")
97            .arg("--compact")
98            .arg(path)
99            .output()
100            .context("Failed to run st for glob")?;
101
102        Ok(String::from_utf8_lossy(&output.stdout).to_string())
103    }
104
105    /// Analyze directory (replaces basic tree viewing)
106    pub fn analyze(&self, path: &Path, mode: &str, depth: usize) -> Result<String> {
107        let output = Command::new(&self.st_binary)
108            .arg("--mode")
109            .arg(mode)
110            .arg("--depth")
111            .arg(depth.to_string())
112            .arg(path)
113            .output()
114            .context("Failed to run st for analysis")?;
115
116        Ok(String::from_utf8_lossy(&output.stdout).to_string())
117    }
118
119    /// Get file/directory stats
120    pub fn stats(&self, path: &Path) -> Result<String> {
121        let output = Command::new(&self.st_binary)
122            .arg("--mode")
123            .arg("stats")
124            .arg("--depth")
125            .arg("0")
126            .arg(path)
127            .output()
128            .context("Failed to run st for stats")?;
129
130        Ok(String::from_utf8_lossy(&output.stdout).to_string())
131    }
132
133    /// Semantic analysis (unique to ST!)
134    pub fn semantic_analyze(&self, path: &Path) -> Result<String> {
135        let output = Command::new(&self.st_binary)
136            .arg("--mode")
137            .arg("semantic")
138            .arg("--depth")
139            .arg("0")
140            .arg(path)
141            .output()
142            .context("Failed to run st for semantic analysis")?;
143
144        Ok(String::from_utf8_lossy(&output.stdout).to_string())
145    }
146
147    /// Quick overview (replaces quick checks)
148    pub fn quick(&self, path: &Path) -> Result<String> {
149        let output = Command::new(&self.st_binary)
150            .arg("--mode")
151            .arg("summary-ai")
152            .arg("--depth")
153            .arg("3")
154            .arg(path)
155            .output()
156            .context("Failed to run st for quick view")?;
157
158        Ok(String::from_utf8_lossy(&output.stdout).to_string())
159    }
160
161    /// Project understanding (replaces multiple analysis tools)
162    pub fn understand_project(&self, path: &Path) -> Result<String> {
163        let results = [
164            "=== QUICK OVERVIEW ===".to_string(),
165            self.quick(path)?,
166            "\n=== SEMANTIC GROUPS ===".to_string(),
167            self.semantic_analyze(path)?,
168            "\n=== STATISTICS ===".to_string(),
169            self.stats(path)?,
170        ];
171
172        Ok(results.join("\n"))
173    }
174}