vtcode_core/
bash_runner.rs1use anyhow::{Context, Result};
8use std::path::PathBuf;
9use std::process::Command;
10
11#[derive(Clone)]
13pub struct BashRunner {
14 working_dir: PathBuf,
16}
17
18impl BashRunner {
19 pub fn new(working_dir: PathBuf) -> Self {
21 Self { working_dir }
22 }
23
24 pub fn cd(&mut self, path: &str) -> Result<()> {
26 let new_path = if path.starts_with('/') {
27 PathBuf::from(path)
28 } else {
29 self.working_dir.join(path)
30 };
31
32 if !new_path.exists() {
33 return Err(anyhow::anyhow!("Directory does not exist: {}", path));
34 }
35
36 if !new_path.is_dir() {
37 return Err(anyhow::anyhow!("Path is not a directory: {}", path));
38 }
39
40 self.working_dir = new_path.canonicalize()?;
41 Ok(())
42 }
43
44 pub fn ls(&self, path: Option<&str>, show_hidden: bool) -> Result<String> {
46 let target_path = path
47 .map(|p| self.resolve_path(p))
48 .unwrap_or_else(|| self.working_dir.clone());
49
50 let mut cmd = Command::new("ls");
51 if show_hidden {
52 cmd.arg("-la");
53 } else {
54 cmd.arg("-l");
55 }
56 cmd.arg(&target_path);
57
58 let output = cmd
59 .output()
60 .with_context(|| "Failed to execute ls command".to_string())?;
61
62 if output.status.success() {
63 Ok(String::from_utf8_lossy(&output.stdout).to_string())
64 } else {
65 Err(anyhow::anyhow!(
66 "ls failed: {}",
67 String::from_utf8_lossy(&output.stderr)
68 ))
69 }
70 }
71
72 pub fn pwd(&self) -> String {
74 self.working_dir.to_string_lossy().to_string()
75 }
76
77 pub fn mkdir(&self, path: &str, parents: bool) -> Result<()> {
79 let target_path = self.resolve_path(path);
80
81 let mut cmd = Command::new("mkdir");
82 if parents {
83 cmd.arg("-p");
84 }
85 cmd.arg(&target_path);
86
87 let output = cmd
88 .output()
89 .with_context(|| "Failed to execute mkdir command".to_string())?;
90
91 if output.status.success() {
92 Ok(())
93 } else {
94 Err(anyhow::anyhow!(
95 "mkdir failed: {}",
96 String::from_utf8_lossy(&output.stderr)
97 ))
98 }
99 }
100
101 pub fn rm(&self, path: &str, recursive: bool, force: bool) -> Result<()> {
103 let target_path = self.resolve_path(path);
104
105 let mut cmd = Command::new("rm");
106 if recursive {
107 cmd.arg("-r");
108 }
109 if force {
110 cmd.arg("-f");
111 }
112 cmd.arg(&target_path);
113
114 let output = cmd
115 .output()
116 .with_context(|| "Failed to execute rm command".to_string())?;
117
118 if output.status.success() {
119 Ok(())
120 } else {
121 Err(anyhow::anyhow!(
122 "rm failed: {}",
123 String::from_utf8_lossy(&output.stderr)
124 ))
125 }
126 }
127
128 pub fn cp(&self, source: &str, dest: &str, recursive: bool) -> Result<()> {
130 let source_path = self.resolve_path(source);
131 let dest_path = self.resolve_path(dest);
132
133 let mut cmd = Command::new("cp");
134 if recursive {
135 cmd.arg("-r");
136 }
137 cmd.arg(&source_path).arg(&dest_path);
138
139 let output = cmd
140 .output()
141 .with_context(|| "Failed to execute cp command".to_string())?;
142
143 if output.status.success() {
144 Ok(())
145 } else {
146 Err(anyhow::anyhow!(
147 "cp failed: {}",
148 String::from_utf8_lossy(&output.stderr)
149 ))
150 }
151 }
152
153 pub fn mv(&self, source: &str, dest: &str) -> Result<()> {
155 let source_path = self.resolve_path(source);
156 let dest_path = self.resolve_path(dest);
157
158 let output = Command::new("mv")
159 .arg(&source_path)
160 .arg(&dest_path)
161 .output()
162 .with_context(|| "Failed to execute mv command".to_string())?;
163
164 if output.status.success() {
165 Ok(())
166 } else {
167 Err(anyhow::anyhow!(
168 "mv failed: {}",
169 String::from_utf8_lossy(&output.stderr)
170 ))
171 }
172 }
173
174 pub fn grep(&self, pattern: &str, path: Option<&str>, recursive: bool) -> Result<String> {
176 let target_path = path
177 .map(|p| self.resolve_path(p))
178 .unwrap_or_else(|| self.working_dir.clone());
179
180 let mut cmd = Command::new("grep");
181 cmd.arg("-n"); if recursive {
183 cmd.arg("-r");
184 }
185 cmd.arg(pattern).arg(&target_path);
186
187 let output = cmd
188 .output()
189 .with_context(|| "Failed to execute grep command".to_string())?;
190
191 if output.status.success() {
192 Ok(String::from_utf8_lossy(&output.stdout).to_string())
193 } else {
194 let stderr = String::from_utf8_lossy(&output.stderr);
196 if stderr.is_empty() {
197 Ok(String::new()) } else {
199 Err(anyhow::anyhow!("grep failed: {}", stderr))
200 }
201 }
202 }
203
204 pub fn find(
206 &self,
207 path: Option<&str>,
208 name_pattern: Option<&str>,
209 type_filter: Option<&str>,
210 ) -> Result<String> {
211 let target_path = path
212 .map(|p| self.resolve_path(p))
213 .unwrap_or_else(|| self.working_dir.clone());
214
215 let mut cmd = Command::new("find");
216 cmd.arg(&target_path);
217
218 if let Some(pattern) = name_pattern {
219 cmd.arg("-name").arg(pattern);
220 }
221
222 if let Some(type_filter) = type_filter {
223 cmd.arg("-type").arg(type_filter);
224 }
225
226 let output = cmd
227 .output()
228 .with_context(|| "Failed to execute find command".to_string())?;
229
230 if output.status.success() {
231 Ok(String::from_utf8_lossy(&output.stdout).to_string())
232 } else {
233 Err(anyhow::anyhow!(
234 "find failed: {}",
235 String::from_utf8_lossy(&output.stderr)
236 ))
237 }
238 }
239
240 pub fn cat(
242 &self,
243 path: &str,
244 start_line: Option<usize>,
245 end_line: Option<usize>,
246 ) -> Result<String> {
247 let file_path = self.resolve_path(path);
248
249 if let (Some(start), Some(end)) = (start_line, end_line) {
250 let range = format!("{}q;{}q", start, end);
252 let output = Command::new("sed")
253 .arg("-n")
254 .arg(&range)
255 .arg(&file_path)
256 .output()
257 .with_context(|| "Failed to execute sed command".to_string())?;
258
259 if output.status.success() {
260 Ok(String::from_utf8_lossy(&output.stdout).to_string())
261 } else {
262 Err(anyhow::anyhow!(
263 "sed failed: {}",
264 String::from_utf8_lossy(&output.stderr)
265 ))
266 }
267 } else {
268 let output = Command::new("cat")
270 .arg(&file_path)
271 .output()
272 .with_context(|| "Failed to execute cat command".to_string())?;
273
274 if output.status.success() {
275 Ok(String::from_utf8_lossy(&output.stdout).to_string())
276 } else {
277 Err(anyhow::anyhow!(
278 "cat failed: {}",
279 String::from_utf8_lossy(&output.stderr)
280 ))
281 }
282 }
283 }
284
285 pub fn head(&self, path: &str, lines: usize) -> Result<String> {
287 let file_path = self.resolve_path(path);
288
289 let output = Command::new("head")
290 .arg("-n")
291 .arg(lines.to_string())
292 .arg(&file_path)
293 .output()
294 .with_context(|| "Failed to execute head command".to_string())?;
295
296 if output.status.success() {
297 Ok(String::from_utf8_lossy(&output.stdout).to_string())
298 } else {
299 Err(anyhow::anyhow!(
300 "head failed: {}",
301 String::from_utf8_lossy(&output.stderr)
302 ))
303 }
304 }
305
306 pub fn tail(&self, path: &str, lines: usize) -> Result<String> {
307 let file_path = self.resolve_path(path);
308
309 let output = Command::new("tail")
310 .arg("-n")
311 .arg(lines.to_string())
312 .arg(&file_path)
313 .output()
314 .with_context(|| "Failed to execute tail command".to_string())?;
315
316 if output.status.success() {
317 Ok(String::from_utf8_lossy(&output.stdout).to_string())
318 } else {
319 Err(anyhow::anyhow!(
320 "tail failed: {}",
321 String::from_utf8_lossy(&output.stderr)
322 ))
323 }
324 }
325
326 pub fn stat(&self, path: &str) -> Result<String> {
328 let file_path = self.resolve_path(path);
329
330 let output = Command::new("ls")
331 .arg("-la")
332 .arg(&file_path)
333 .output()
334 .with_context(|| "Failed to execute ls command".to_string())?;
335
336 if output.status.success() {
337 Ok(String::from_utf8_lossy(&output.stdout).to_string())
338 } else {
339 Err(anyhow::anyhow!(
340 "stat failed: {}",
341 String::from_utf8_lossy(&output.stderr)
342 ))
343 }
344 }
345
346 pub fn run(&self, command: &str, args: &[&str]) -> Result<String> {
348 let output = Command::new(command)
349 .args(args)
350 .current_dir(&self.working_dir)
351 .output()
352 .with_context(|| format!("Failed to execute command: {}", command))?;
353
354 if output.status.success() {
355 Ok(String::from_utf8_lossy(&output.stdout).to_string())
356 } else {
357 let stderr = String::from_utf8_lossy(&output.stderr);
358 if stderr.is_empty() {
359 Ok(String::new())
360 } else {
361 Err(anyhow::anyhow!("Command failed: {}", stderr))
362 }
363 }
364 }
365
366 fn resolve_path(&self, path: &str) -> PathBuf {
368 if path.starts_with('/') {
369 PathBuf::from(path)
370 } else {
371 self.working_dir.join(path)
372 }
373 }
374}