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