deepseek_rust_cli/tools/file/
refactor.rs1use std::{collections::HashMap, path::Path};
2
3use anyhow::Result;
4use async_trait::async_trait;
5use serde_json::Value;
6
7use crate::{agent::types::UndoAction, tools, tools::base::Tool};
8
9pub struct MoveCodeBlockTool;
10#[async_trait]
11impl Tool for MoveCodeBlockTool {
12 fn name(&self) -> &str {
13 "move_code_block"
14 }
15 async fn execute(
16 &self,
17 args: &HashMap<String, Value>,
18 undo: &mut Vec<UndoAction>,
19 _cwd: Option<&Path>,
20 ) -> Result<String> {
21 let src = args
22 .get("source_path")
23 .and_then(|v| v.as_str())
24 .ok_or_else(|| anyhow::anyhow!("Missing 'source_path'"))?;
25 let dst = args
26 .get("destination_path")
27 .and_then(|v| v.as_str())
28 .ok_or_else(|| anyhow::anyhow!("Missing 'destination_path'"))?;
29 let pattern = args
30 .get("block_pattern")
31 .and_then(|v| v.as_str())
32 .ok_or_else(|| anyhow::anyhow!("Missing 'block_pattern'"))?;
33
34 let src_p = crate::tools::base::validate_path(src)?;
35 let dst_p = crate::tools::base::validate_path(dst)?;
36
37 let src_backup = tokio::fs::read(&src_p).await.ok();
39 let dst_backup = tokio::fs::read(&dst_p).await.ok();
40
41 undo.push(UndoAction {
42 r#type: "replace".to_string(),
43 path: src_p.to_string_lossy().to_string(),
44 backup: src_backup,
45 });
46 undo.push(UndoAction {
47 r#type: "replace".to_string(),
48 path: dst_p.to_string_lossy().to_string(),
49 backup: dst_backup,
50 });
51
52 tools::file_ops::move_code_block(src_p.to_str().unwrap(), dst_p.to_str().unwrap(), pattern)
53 .await
54 }
55}
56
57pub struct SplitFileTool;
58#[async_trait]
59impl Tool for SplitFileTool {
60 fn name(&self) -> &str {
61 "split_file"
62 }
63 async fn execute(
64 &self,
65 args: &HashMap<String, Value>,
66 _undo: &mut Vec<UndoAction>,
67 _cwd: Option<&Path>,
68 ) -> Result<String> {
69 let path = args
70 .get("file_path")
71 .and_then(|v| v.as_str())
72 .ok_or_else(|| anyhow::anyhow!("Missing 'file_path'"))?;
73 let pattern = args
74 .get("split_pattern")
75 .and_then(|v| v.as_str())
76 .ok_or_else(|| anyhow::anyhow!("Missing 'split_pattern'"))?;
77 let prefix = args
78 .get("output_prefix")
79 .and_then(|v| v.as_str())
80 .ok_or_else(|| anyhow::anyhow!("Missing 'output_prefix'"))?;
81
82 tools::file_io::split_file(path, pattern, prefix).await
83 }
84}
85
86pub struct CleanupFileTool;
87#[async_trait]
88impl Tool for CleanupFileTool {
89 fn name(&self) -> &str {
90 "cleanup_file"
91 }
92 async fn execute(
93 &self,
94 args: &HashMap<String, Value>,
95 undo: &mut Vec<UndoAction>,
96 _cwd: Option<&Path>,
97 ) -> Result<String> {
98 let path = args
99 .get("file_path")
100 .and_then(|v| v.as_str())
101 .ok_or_else(|| anyhow::anyhow!("Missing 'file_path'"))?;
102
103 let p = crate::tools::base::validate_path(path)?;
104 let backup = tokio::fs::read(&p).await.ok();
105 undo.push(UndoAction {
106 r#type: "replace".to_string(),
107 path: p.to_string_lossy().to_string(),
108 backup,
109 });
110
111 tools::file_io::cleanup_file(p.to_str().unwrap()).await
112 }
113}
114
115pub struct ProjectCheckpointTool;
116#[async_trait]
117impl Tool for ProjectCheckpointTool {
118 fn name(&self) -> &str {
119 "project_checkpoint"
120 }
121 async fn execute(
122 &self,
123 args: &HashMap<String, Value>,
124 _undo: &mut Vec<UndoAction>,
125 _cwd: Option<&Path>,
126 ) -> Result<String> {
127 let name = args
128 .get("name")
129 .and_then(|v| v.as_str())
130 .ok_or_else(|| anyhow::anyhow!("Missing 'name'"))?;
131
132 if !name
133 .chars()
134 .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
135 {
136 anyhow::bail!("Invalid checkpoint name: only alphanumeric, '_' and '-' are allowed");
137 }
138
139 let checkpoint_dir = crate::tools::base::validate_path(".deep/checkpoints")?;
140 if !checkpoint_dir.exists() {
141 std::fs::create_dir_all(&checkpoint_dir)?;
142 }
143
144 let timestamp = chrono::Local::now().format("%Y%m%d_%H%M%S");
145 let filename = format!("{}_{}.tar.gz", name, timestamp);
146 let path = checkpoint_dir.join(&filename);
147
148 let output = tokio::process::Command::new("tar")
150 .args([
151 "-czf",
152 path.to_str().unwrap(),
153 "src",
154 "Cargo.toml",
155 "README.md",
156 ])
157 .output()
158 .await?;
159
160 if output.status.success() {
161 Ok(format!(
162 "Project checkpoint '{}' created successfully.",
163 filename
164 ))
165 } else {
166 Err(anyhow::anyhow!(
167 "Failed to create checkpoint: {}",
168 String::from_utf8_lossy(&output.stderr)
169 ))
170 }
171 }
172}
173
174pub struct RestoreCheckpointTool;
175#[async_trait]
176impl Tool for RestoreCheckpointTool {
177 fn name(&self) -> &str {
178 "restore_checkpoint"
179 }
180 async fn execute(
181 &self,
182 args: &HashMap<String, Value>,
183 _undo: &mut Vec<UndoAction>,
184 _cwd: Option<&Path>,
185 ) -> Result<String> {
186 let name = args
187 .get("checkpoint_file")
188 .and_then(|v| v.as_str())
189 .ok_or_else(|| anyhow::anyhow!("Missing 'checkpoint_file'"))?;
190
191 let checkpoint_dir = crate::tools::base::validate_path(".deep/checkpoints")?;
192 let path = checkpoint_dir.join(name);
193 let validated_path = crate::tools::base::validate_path(path.to_str().unwrap())?;
194
195 if !validated_path.starts_with(&checkpoint_dir) {
196 anyhow::bail!("Access to checkpoint file denied: path traversal detected");
197 }
198
199 let output = tokio::process::Command::new("tar")
200 .args(["-xzf", validated_path.to_str().unwrap()])
201 .output()
202 .await?;
203
204 if output.status.success() {
205 Ok(format!("Project restored from checkpoint '{}'.", name))
206 } else {
207 Err(anyhow::anyhow!(
208 "Failed to restore checkpoint: {}",
209 String::from_utf8_lossy(&output.stderr)
210 ))
211 }
212 }
213}
214
215pub struct ProjectWideReplaceTool;
216#[async_trait]
217impl Tool for ProjectWideReplaceTool {
218 fn name(&self) -> &str {
219 "project_wide_replace"
220 }
221 async fn execute(
222 &self,
223 args: &HashMap<String, Value>,
224 _undo: &mut Vec<UndoAction>,
225 _cwd: Option<&Path>,
226 ) -> Result<String> {
227 let old_text = args
228 .get("old_text")
229 .and_then(|v| v.as_str())
230 .ok_or_else(|| anyhow::anyhow!("Missing 'old_text'"))?;
231 let new_text = args
232 .get("new_text")
233 .and_then(|v| v.as_str())
234 .ok_or_else(|| anyhow::anyhow!("Missing 'new_text'"))?;
235 let glob_pattern = args
236 .get("glob")
237 .and_then(|v| v.as_str())
238 .unwrap_or("**/*.rs");
239
240 let mut count = 0;
241 let mut file_count = 0;
242
243 if let Ok(paths) = glob::glob(glob_pattern) {
245 for entry in paths.filter_map(|e| e.ok()) {
246 if entry.is_file() {
247 let path_str = entry.to_string_lossy().to_string();
248 if let Ok(validated_path) = crate::tools::base::validate_path(&path_str) {
249 let path_str_val = validated_path.to_string_lossy();
250 if !path_str_val.contains("target") && !path_str_val.contains(".git") {
251 if let Ok(content) = std::fs::read_to_string(&validated_path) {
252 if content.contains(old_text) {
253 let new_content = content.replace(old_text, new_text);
254 std::fs::write(&validated_path, new_content)?;
255 file_count += 1;
256 count += content.matches(old_text).count();
257 }
258 }
259 }
260 }
261 }
262 }
263 }
264
265 Ok(format!(
266 "Replaced {} occurrences in {} files matching '{}'.",
267 count, file_count, glob_pattern
268 ))
269 }
270}