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        let src_p = crate::tools::base::validate_path(src)?;
35        let dst_p = crate::tools::base::validate_path(dst)?;
36
37        // Backup both files
38        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        // Use tar via shell
149        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        // Use glob to match files
244        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}