Skip to main content

deepseek_rust_cli/tools/file/
refactor.rs

1use 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        // Backup both files
35        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        // Use tar via shell
145        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        // Use glob to match files
240        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}