1use std::fs::{self, OpenOptions};
10use std::io::{Read, Write};
11use std::path::{Path, PathBuf};
12
13use anyhow::{Context, Result};
14use fs2::FileExt;
15use indexmap::IndexMap;
16use serde::{Deserialize, Serialize};
17
18#[derive(Clone)]
20pub struct MarkdownStorage {
21 storage_dir: PathBuf,
22}
23
24impl MarkdownStorage {
25 pub fn new(storage_dir: PathBuf) -> Self {
27 Self { storage_dir }
28 }
29
30 pub fn init(&self) -> Result<()> {
32 fs::create_dir_all(&self.storage_dir)?;
33 Ok(())
34 }
35
36 pub fn store<T: Serialize>(&self, key: &str, data: &T, title: &str) -> Result<()> {
38 let file_path = self.storage_dir.join(format!("{key}.md"));
39 let markdown = self.serialize_to_markdown(data, title)?;
40 write_with_lock(&file_path, markdown.as_bytes())
41 }
42
43 pub fn load<T: for<'de> Deserialize<'de>>(&self, key: &str) -> Result<T> {
45 let file_path = self.storage_dir.join(format!("{key}.md"));
46 let content = read_with_shared_lock(&file_path)?;
47 self.deserialize_from_markdown(&content)
48 }
49
50 pub fn list(&self) -> Result<Vec<String>> {
52 let mut items = Vec::new();
53
54 for entry in fs::read_dir(&self.storage_dir)? {
55 let entry = entry?;
56 if let Some(name) = entry.path().file_stem().and_then(|file_name| file_name.to_str()) {
57 items.push(name.to_string());
58 }
59 }
60
61 Ok(items)
62 }
63
64 pub fn delete(&self, key: &str) -> Result<()> {
66 let file_path = self.storage_dir.join(format!("{key}.md"));
67 if file_path.exists() {
68 if let Ok(file) = OpenOptions::new().read(true).write(true).open(&file_path) {
71 let _ = file.lock_exclusive();
72 drop(file);
74 }
75
76 match fs::remove_file(&file_path) {
79 Ok(_) => {}
80 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
81 Err(err) => {
82 return Err(err)
83 .with_context(|| format!("Failed to delete markdown file at {}", file_path.display()));
84 }
85 }
86 }
87 Ok(())
88 }
89
90 pub fn exists(&self, key: &str) -> bool {
92 let file_path = self.storage_dir.join(format!("{key}.md"));
93 file_path.exists()
94 }
95
96 fn serialize_to_markdown<T: Serialize>(&self, data: &T, title: &str) -> Result<String> {
97 let json = serde_json::to_string_pretty(data)?;
98 let yaml = serde_saphyr::to_string(data)?;
99
100 let markdown = format!(
101 "# {}\n\n\
102 ## JSON\n\n\
103 ```json\n\
104 {}\n\
105 ```\n\n\
106 ## YAML\n\n\
107 ```yaml\n\
108 {}\n\
109 ```\n\n\
110 ## Raw Data\n\n\
111 {}\n",
112 title,
113 json,
114 yaml,
115 self.format_raw_data(data)
116 );
117
118 Ok(markdown)
119 }
120
121 fn deserialize_from_markdown<T: for<'de> Deserialize<'de>>(&self, content: &str) -> Result<T> {
122 if let Some(json_block) = self.extract_code_block(content, "json") {
123 return serde_json::from_str(json_block).context("Failed to parse JSON from markdown");
124 }
125
126 if let Some(yaml_block) = self.extract_code_block(content, "yaml") {
127 return serde_saphyr::from_str(yaml_block).context("Failed to parse YAML from markdown");
128 }
129
130 Err(anyhow::anyhow!("No valid JSON or YAML found in markdown"))
131 }
132
133 fn extract_code_block<'a>(&self, content: &'a str, language: &str) -> Option<&'a str> {
134 let start_pattern = format!("```{language}");
135 let end_pattern = "```";
136
137 if let Some(start_idx) = content.find(&start_pattern) {
138 let code_start = start_idx + start_pattern.len();
139 if let Some(end_idx) = content[code_start..].find(end_pattern) {
140 let code_end = code_start + end_idx;
141 return Some(content[code_start..code_end].trim());
142 }
143 }
144
145 None
146 }
147
148 fn format_raw_data<T: Serialize>(&self, data: &T) -> String {
149 match serde_json::to_value(data) {
150 Ok(serde_json::Value::Object(map)) => {
151 let mut lines = Vec::with_capacity(map.len());
152 for (key, value) in map {
153 lines.push(format!("- **{}**: {}", key, self.format_value(&value)));
154 }
155 lines.join("\n")
156 }
157 _ => "Complex data structure".to_string(),
158 }
159 }
160
161 fn format_value(&self, value: &serde_json::Value) -> String {
162 match value {
163 serde_json::Value::String(s) => format!("\"{s}\""),
164 serde_json::Value::Number(n) => n.to_string(),
165 serde_json::Value::Bool(b) => b.to_string(),
166 serde_json::Value::Array(arr) => format!("[{} items]", arr.len()),
167 serde_json::Value::Object(obj) => format!("{{{} fields}}", obj.len()),
168 serde_json::Value::Null => "null".to_string(),
169 }
170 }
171}
172
173fn write_with_lock(path: &Path, data: &[u8]) -> Result<()> {
174 if let Some(parent) = path.parent() {
175 fs::create_dir_all(parent)
176 .with_context(|| format!("Failed to ensure parent directory exists for {}", path.display()))?;
177 }
178
179 let mut file = OpenOptions::new()
180 .create(true)
181 .write(true)
182 .truncate(false)
183 .open(path)
184 .with_context(|| format!("Failed to open file at {}", path.display()))?;
185
186 FileExt::lock_exclusive(&file)
187 .with_context(|| format!("Failed to acquire exclusive lock for {}", path.display()))?;
188
189 file.set_len(0)
190 .with_context(|| format!("Failed to truncate file at {} while holding exclusive lock", path.display()))?;
191
192 file.write_all(data)
193 .with_context(|| format!("Failed to write file content to {} while holding exclusive lock", path.display()))?;
194
195 file.sync_all()
196 .with_context(|| format!("Failed to sync file at {} after writing with exclusive lock", path.display()))?;
197
198 FileExt::unlock(&file).with_context(|| format!("Failed to release exclusive lock for {}", path.display()))
199}
200
201fn read_with_shared_lock(path: &Path) -> Result<String> {
202 let mut file = OpenOptions::new()
203 .read(true)
204 .open(path)
205 .with_context(|| format!("Failed to open file at {}", path.display()))?;
206
207 FileExt::lock_shared(&file).with_context(|| format!("Failed to acquire shared lock for {}", path.display()))?;
208
209 let mut content = String::new();
210 file.read_to_string(&mut content)
211 .with_context(|| format!("Failed to read file content from {} while holding shared lock", path.display()))?;
212
213 FileExt::unlock(&file).with_context(|| format!("Failed to release shared lock for {}", path.display()))?;
214
215 Ok(content)
216}
217
218pub struct SimpleKVStorage {
220 storage: MarkdownStorage,
221}
222
223impl SimpleKVStorage {
224 pub fn new(storage_dir: PathBuf) -> Self {
225 Self { storage: MarkdownStorage::new(storage_dir) }
226 }
227
228 pub fn init(&self) -> Result<()> {
229 self.storage.init()
230 }
231
232 pub fn put(&self, key: &str, value: &str) -> Result<()> {
233 let data = IndexMap::from([("value".to_string(), value.to_string())]);
234 self.storage.store(key, &data, &format!("Key-Value: {key}"))
235 }
236
237 pub fn get(&self, key: &str) -> Result<String> {
238 let data: IndexMap<String, String> = self.storage.load(key)?;
239 data.get("value")
240 .cloned()
241 .ok_or_else(|| anyhow::anyhow!("Value not found for key: {key}"))
242 }
243
244 pub fn delete(&self, key: &str) -> Result<()> {
245 self.storage.delete(key)
246 }
247
248 pub fn list_keys(&self) -> Result<Vec<String>> {
249 self.storage.list()
250 }
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct ProjectData {
256 pub name: String,
257 pub description: Option<String>,
258 pub version: String,
259 pub tags: Vec<String>,
260 pub metadata: IndexMap<String, String>,
261}
262
263impl ProjectData {
264 pub fn new(name: &str) -> Self {
265 Self {
266 name: name.to_string(),
267 description: None,
268 version: "1.0.0".to_string(),
269 tags: vec![],
270 metadata: IndexMap::new(),
271 }
272 }
273}
274
275#[derive(Clone)]
277pub struct ProjectStorage {
278 storage: MarkdownStorage,
279}
280
281impl ProjectStorage {
282 pub fn new(storage_dir: PathBuf) -> Self {
283 Self { storage: MarkdownStorage::new(storage_dir) }
284 }
285
286 pub fn init(&self) -> Result<()> {
287 self.storage.init()
288 }
289
290 pub fn save_project(&self, project: &ProjectData) -> Result<()> {
291 self.storage
292 .store(&project.name, project, &format!("Project: {}", project.name))
293 }
294
295 pub fn load_project(&self, name: &str) -> Result<ProjectData> {
296 self.storage.load(name)
297 }
298
299 pub fn list_projects(&self) -> Result<Vec<String>> {
300 self.storage.list()
301 }
302
303 pub fn delete_project(&self, name: &str) -> Result<()> {
304 self.storage.delete(name)
305 }
306
307 pub fn storage_dir(&self) -> &Path {
308 &self.storage.storage_dir
309 }
310}
311
312#[derive(Clone)]
314pub struct SimpleProjectManager {
315 storage: ProjectStorage,
316 workspace_root: PathBuf,
317 project_root: PathBuf,
318}
319
320impl SimpleProjectManager {
321 pub fn new(workspace_root: PathBuf) -> Self {
324 let project_root = workspace_root.join(".vtcode").join("projects");
325 Self::with_project_root(workspace_root, project_root)
326 }
327
328 pub fn with_project_root(workspace_root: PathBuf, project_root: PathBuf) -> Self {
330 let storage = ProjectStorage::new(project_root.clone());
331 Self { storage, workspace_root, project_root }
332 }
333
334 pub fn init(&self) -> Result<()> {
336 self.storage.init()
337 }
338
339 pub fn create_project(&self, name: &str, description: Option<&str>) -> Result<()> {
341 let mut project = ProjectData::new(name);
342 project.description = description.map(|s| s.to_string());
343
344 self.storage.save_project(&project)?;
345 Ok(())
346 }
347
348 pub fn load_project(&self, name: &str) -> Result<ProjectData> {
350 self.storage.load_project(name)
351 }
352
353 pub fn list_projects(&self) -> Result<Vec<String>> {
355 self.storage.list_projects()
356 }
357
358 pub fn delete_project(&self, name: &str) -> Result<()> {
360 self.storage.delete_project(name)
361 }
362
363 pub fn update_project(&self, project: &ProjectData) -> Result<()> {
365 self.storage.save_project(project)
366 }
367
368 pub fn project_data_dir(&self, project_name: &str) -> PathBuf {
370 self.project_root.join(project_name)
371 }
372
373 pub fn config_dir(&self, project_name: &str) -> PathBuf {
375 self.project_data_dir(project_name).join("config")
376 }
377
378 pub fn cache_dir(&self, project_name: &str) -> PathBuf {
380 self.project_data_dir(project_name).join("cache")
381 }
382
383 pub fn workspace_root(&self) -> &Path {
385 &self.workspace_root
386 }
387
388 pub fn project_root(&self) -> &Path {
390 &self.project_root
391 }
392
393 pub fn project_exists(&self, name: &str) -> bool {
395 self.storage
396 .list_projects()
397 .map(|projects| projects.contains(&name.to_string()))
398 .unwrap_or(false)
399 }
400
401 pub fn get_project_info(&self, name: &str) -> Result<String> {
403 let project = self.load_project(name)?;
404
405 let mut info = format!("Project: {}\n", project.name);
406 if let Some(desc) = &project.description {
407 info.push_str(&format!("Description: {desc}\n"));
408 }
409 info.push_str(&format!("Version: {}\n", project.version));
410 info.push_str(&format!("Tags: {}\n", project.tags.join(", ")));
411
412 if !project.metadata.is_empty() {
413 info.push_str("\nMetadata:\n");
414 for (key, value) in &project.metadata {
415 info.push_str(&format!(" {key}: {value}\n"));
416 }
417 }
418
419 Ok(info)
420 }
421
422 pub fn identify_current_project(&self) -> Result<String> {
424 let project_file = self.workspace_root.join(".vtcode-project");
425 if project_file.exists() {
426 let content = fs::read_to_string(&project_file)?;
427 return Ok(content.trim().to_string());
428 }
429
430 self.workspace_root
431 .file_name()
432 .and_then(|name| name.to_str())
433 .map(|name| name.to_string())
434 .ok_or_else(|| anyhow::anyhow!("Could not determine project name from directory"))
435 }
436
437 pub fn set_current_project(&self, name: &str) -> Result<()> {
439 let project_file = self.workspace_root.join(".vtcode-project");
440 fs::write(project_file, name)?;
441 Ok(())
442 }
443}
444
445pub struct SimpleCache {
447 cache_dir: PathBuf,
448}
449
450impl SimpleCache {
451 pub fn new(cache_dir: PathBuf) -> Self {
453 Self { cache_dir }
454 }
455
456 pub fn init(&self) -> Result<()> {
458 fs::create_dir_all(&self.cache_dir)?;
459 Ok(())
460 }
461
462 pub fn store(&self, key: &str, data: &str) -> Result<()> {
464 let file_path = self.cache_dir.join(format!("{key}.txt"));
465 write_with_lock(&file_path, data.as_bytes())
466 }
467
468 pub fn load(&self, key: &str) -> Result<String> {
470 let file_path = self.cache_dir.join(format!("{key}.txt"));
471 read_with_shared_lock(&file_path).map_err(|err| {
472 if err
473 .downcast_ref::<std::io::Error>()
474 .is_some_and(|io_err| io_err.kind() == std::io::ErrorKind::NotFound)
475 {
476 anyhow::anyhow!("Cache key '{key}' not found")
477 } else {
478 err
479 }
480 })
481 }
482
483 pub fn exists(&self, key: &str) -> bool {
485 let file_path = self.cache_dir.join(format!("{key}.txt"));
486 file_path.exists()
487 }
488
489 pub fn clear(&self) -> Result<()> {
491 for entry in fs::read_dir(&self.cache_dir)? {
492 let entry = entry?;
493 if entry.path().is_file() {
494 fs::remove_file(entry.path())?;
495 }
496 }
497 Ok(())
498 }
499
500 pub fn list(&self) -> Result<Vec<String>> {
502 let mut entries = Vec::new();
503 for entry in fs::read_dir(&self.cache_dir)? {
504 let entry = entry?;
505 if let Some(name) = entry.path().file_stem().and_then(|file_name| file_name.to_str()) {
506 entries.push(name.to_string());
507 }
508 }
509 Ok(entries)
510 }
511}